package com.cloudkinto.service.order.outbound.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cloudkinto.bo.api.ImportUpdateTrackingNumberAutoSendApiBo;
import com.cloudkinto.bo.api.TransferSendApiBo;
import com.cloudkinto.bo.order.outbound.*;
import com.cloudkinto.bo.stock.BoxStockByTypeQueryBo;
import com.cloudkinto.bo.stock.ProductStockBySkuQueryBo;
import com.cloudkinto.bo.storage.CellAvailableByBoxQueryBo;
import com.cloudkinto.bo.storage.CellAvailableBySkuQueryBo;
import com.cloudkinto.bo.storage.StorageLocationQueryBo;
import com.cloudkinto.common.BeanConvert;
import com.cloudkinto.common.RabbitQueueConstant;
import com.cloudkinto.common.RedisKeyConstants;
import com.cloudkinto.common.common.DateUtils;
import com.cloudkinto.common.constant.SysConstant;
import com.cloudkinto.common.exception.BizException;
import com.cloudkinto.common.utils.StringsUtil;
import com.cloudkinto.common.utils.dict.StaticDict;
import com.cloudkinto.common.web.PageResult;
import com.cloudkinto.common.web.SingleResult;
import com.cloudkinto.dao.OrderOutboundDao;
import com.cloudkinto.dao.ProductDao;
import com.cloudkinto.dao.ProductStockDao;
import com.cloudkinto.dto.order.outbound.SysStockDeductDto;
import com.cloudkinto.dto.product.ProductMinSizeDto;
import com.cloudkinto.dto.stock.query.BoxTypeStockPageDto;
import com.cloudkinto.dto.stock.query.SkuStockPageDto;
import com.cloudkinto.entity.*;
import com.cloudkinto.enumType.common.CountryEnum;
import com.cloudkinto.enumType.common.ProgressEnum;
import com.cloudkinto.enumType.operate.OperateTypeEnum;
import com.cloudkinto.enumType.order.outbound.*;
import com.cloudkinto.enumType.stock.StockOrderTypeEnum;
import com.cloudkinto.enumType.stock.StockTypeEnum;
import com.cloudkinto.enumType.storage.StorageAreaEnum;
import com.cloudkinto.service.address.AddressBookService;
import com.cloudkinto.service.channel.LogisticsChannelService;
import com.cloudkinto.service.company.CompanyService;
import com.cloudkinto.service.consumer.vo.order.PlatOrderCancelConsumerReq;
import com.cloudkinto.service.consumer.vo.order.PlatOrderSendConsumerReq;
import com.cloudkinto.service.consumer.vo.order.PlatformOrderTrackNoEntity;
import com.cloudkinto.service.consumer.vo.order.UpdateWarehouseReq;
import com.cloudkinto.service.cost.CostService;
import com.cloudkinto.service.event.order.*;
import com.cloudkinto.service.event.stock.SysStockDeductEvent;
import com.cloudkinto.service.file.FileService;
import com.cloudkinto.service.material.MaterialService;
import com.cloudkinto.service.operate.OperateRecordService;
import com.cloudkinto.service.order.box.OrderBoxService;
import com.cloudkinto.service.order.outbound.*;
import com.cloudkinto.service.order.outbound.vo.LabellaOrderTrackNumberDto;
import com.cloudkinto.service.order.outbound.vo.OrderImportErrorVo1;
import com.cloudkinto.service.order.outbound.vo.OrderOutputExcelImportNew;
import com.cloudkinto.service.order.picking.OutboundPickingService;
import com.cloudkinto.service.platform.PlatformOrderPackageDetailService;
import com.cloudkinto.service.platform.PlatformStoreService;
import com.cloudkinto.service.product.ProductCategoryService;
import com.cloudkinto.service.product.ProductService;
import com.cloudkinto.service.progress.ProgressService;
import com.cloudkinto.service.regular.vo.OrderPackageRes;
import com.cloudkinto.service.regular.vo.ProductMemoMsg;
import com.cloudkinto.service.stock.ProductStockFlowingService;
import com.cloudkinto.service.stock.ProductStockRackService;
import com.cloudkinto.service.storage.StorageLocationService;
import com.cloudkinto.service.storage.impl.StorageLocationServiceImpl;
import com.cloudkinto.service.system.SystemSetService;
import com.cloudkinto.service.user.UserService;
import com.cloudkinto.service.warehouse.WarehouseService;
import com.cloudkinto.service.wr.QuotationSchemeService;
import com.cloudkinto.utils.BeanCopierUtils;
import com.cloudkinto.vo.api.TransferOutboundApiVo;
import com.cloudkinto.vo.common.CommonBatchOperationVo;
import com.cloudkinto.vo.common.ListPageStatusStaDto;
import com.cloudkinto.vo.cost.out.CostItemForOrderPageDto;
import com.cloudkinto.vo.file.FileRes;
import com.cloudkinto.vo.file.FileVo;
import com.cloudkinto.vo.order.outbound.*;
import com.cloudkinto.vo.stock.ProductFlowingOutEt;
import com.cloudkinto.vo.storage.CellAvailableVo;
import com.cloudkinto.vo.storage.StorageLocationVo;
import com.cloudkinto.vo.system.omp.OmpSystemEntity;
import com.cloudkinto.vo.wr.cost.OrderOutCostReq;
import com.cloudkinto.vo.wr.trial.*;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.RequiredArgsConstructor;
import okhttp3.*;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.cloudkinto.common.utils.OkHttpUtils.getUnsafeOkHttpClient;
import static com.cloudkinto.common.utils.RakutenUtil.createSPHeader;

/**
 * <p>
 * 出库单 service实现类
 * </p>
 *
 * @author long
 * @since 2024-11-21
 */
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class OutboundServiceImpl extends ServiceImpl<OrderOutboundDao, OrderOutboundDo> implements OutboundService {

    private final RedisTemplate redisTemplate;
    private final ProgressService progressService;
    private final OperateRecordService operateRecordService;
    private final OrderOutboundProductService outboundProductService;
    private final OutboundPickingService outboundPickingService;
    private final ProductStockDao productStockDao;
    private final ProductStockRackService productStockRackService;
    private final WarehouseService warehouseService;
    private final PlatformStoreService platformStoreService;
    private final CompanyService companyService;
    private final LogisticsChannelService logisticsChannelService;
    private final ProductCategoryService productCategoryService;
    private final OrderOutboundPackageService outboundPackageService;
    private final OrderOutboundPackageDetailService outboundPackageDetailService;
    private final OrderOutboundPackageMaterialService outboundPackageMaterialService;
    private final ProductService productService;
    private final OrderBoxService boxService;
    private final StorageLocationService storageLocationService;
    private final OutboundBoxService outboundBoxService;
    private final OutboundBoxProductService outboundBoxProductService;
    private final OutboundWaveService waveService;
    private final MaterialService materialService;
    private final OutboundCancelService cancelService;
    private final QuotationSchemeService quotationSchemeService;
    private final FileService fileService;
    private final SystemSetService systemSetService;
    private final CostService costService;
    private final ProductStockFlowingService stockFlowingService;
    private final UserService userService;
    private final RabbitTemplate rabbitTemplate;
    private final ApplicationContext context;
    private final TransactionTemplate transactionTemplate;
    private final ProductDao productDao;
    private final PlatformOrderPackageDetailService platformOrderPackageDetailService;
    private final RedissonClient redissonClient;

    @Value("${gbc.customer}")
    private String customer;
    @Value("${gbc.jdy-service-url}")
    private String jdyServiceUrl;

    private final AddressBookService addressBookService;

    @Resource
    @Lazy
    private OutboundImportExportService outboundImportExportService;
    private final ThreadPoolExecutor threadPoolExecutor;

    /**
     * 对于筋斗云的仓库 需要推送到筋斗云
     */
    @Override
    public void pushToJdCloud() {
        if (!StaticDict.Company_Type.Labella.getValue().equals(customer) && !StaticDict.Company_Type.Lemooli.getValue().equals(customer)) {
            return;
        }
        List<OrderOutboundDo> list = baseMapper.selectList(new LambdaQueryWrapper<OrderOutboundDo>()
                .ne(OrderOutboundDo::getStatus, 0).ne(OrderOutboundDo::getStatus, 7).eq(OrderOutboundDo::getOrderType, 1)
                .in(OrderOutboundDo::getSendOut, 1, 3).ge(OrderOutboundDo::getCreateTime, DateUtils.dateFormatToString(DateUtils.passingDate(new Date(), -7))));
        if (list.isEmpty()) {
            return;
        }
        List<List<OrderOutboundDo>> partition = ListUtil.partition(list, 2000);
        for (List<OrderOutboundDo> orderOutboundDos : partition) {
            rabbitTemplate.convertAndSend(RabbitQueueConstant.OutBound_Labella_Order_PushTo_Jdy, JSON.toJSONString(orderOutboundDos));
        }
    }

    @Override
    public void consumerPushToJdCloud(List<OrderOutboundDo> list) {
        if (list.isEmpty()) {
            return;
        }
        //产品信息
        Map<Long, List<OrderOutboundProductDo>> productMap = outboundProductService.list(new LambdaQueryWrapper<OrderOutboundProductDo>()
                        .in(OrderOutboundProductDo::getOutboundId, list.stream().map(OrderOutboundDo::getId).collect(Collectors.toList())))
                .stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getOutboundId));
        List<Long> productIds = new ArrayList<>();
        productMap.values().forEach(list1 -> productIds.addAll(list1.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList())));
        Map<Long, String> productSkuMap = new HashMap<>();
        if (!productIds.isEmpty()) {
            productSkuMap = productService.list(new LambdaQueryWrapper<ProductDo>().in(ProductDo::getId, productIds)).stream().collect(Collectors.toMap(ProductDo::getId, ProductDo::getSku));
        }
        Map<Long, List<OrderOutboundDo>> orderMap = list.stream().collect(Collectors.groupingBy(OrderOutboundDo::getCompanyId));

        Map<String, List<Long>> failMap = new HashMap<>();
        Map<String, Long> orderIdMap = new HashMap<>();
        List<OrderOutputExcelImportNew> exportList = new ArrayList<>();
        for (Long companyId : orderMap.keySet()) {
            for (OrderOutboundDo outDo : orderMap.get(companyId)) {
                List<OrderOutboundProductDo> orderOutboundProductDos = productMap.get(outDo.getId());
                if (orderOutboundProductDos == null) {
                    failMap.computeIfAbsent("无产品信息", k -> new ArrayList<>()).add(outDo.getId());
                    continue;
                }
                for (OrderOutboundProductDo orderOutboundProductDo : orderOutboundProductDos) {
                    OrderOutputExcelImportNew outputEntity = new OrderOutputExcelImportNew();
                    outputEntity.setPlatformNo(outDo.getPlatformOrderNo());
                    outputEntity.setSendDate(DateUtils.dateFormatToString(new Date()));
                    outputEntity.setDeliveryDate(outDo.getDeliveryDate());
                    outputEntity.setDeliveryTime(outDo.getDeliveryTimeName());
                    outputEntity.setReceiverName(outDo.getReceiver());
                    outputEntity.setE8(outDo.getCountryRegionCode());
                    outputEntity.setPostCode(outDo.getPostCode());
                    outputEntity.setShipmentType(outDo.getReserveFlag() != null && outDo.getReserveFlag() == 2 ? 1 : 0);
                    outputEntity.setArea(outDo.getProvinceName());//省/周
                    if (outputEntity.getArea() != null && outputEntity.getArea().length() > 5) {
                        String addr1 = outputEntity.getArea();
                        int index1 = addr1.indexOf("県");
                        int index2 = addr1.indexOf("都");
                        int index3 = addr1.indexOf("道");
                        int index4 = addr1.indexOf("府");
                        List<Integer> indexList = new ArrayList<>();
                        if (index1 != -1) {
                            indexList.add(index1);
                        }
                        if (index2 != -1) {
                            indexList.add(index2);
                        }
                        if (index3 != -1) {
                            indexList.add(index3);
                        }
                        if (index4 != -1) {
                            indexList.add(index4);
                        }
                        Collections.sort(indexList);
                        if (indexList.isEmpty()) {
                            outputEntity.setArea(addr1.substring(0, 3));
                        } else {
                            int firstIndex = indexList.get(0) < 2 ? indexList.get(0) + 1 : indexList.get(0);
                            outputEntity.setArea(addr1.substring(0, firstIndex + 1));
                        }
                    }
                    if (StringUtils.isNotBlank(outputEntity.getArea()) && outputEntity.getArea().length() > 10) {
                        outputEntity.setArea(outputEntity.getArea().substring(0, 10));
                    }
                    outputEntity.setAddress(StringUtils.isNotBlank(outDo.getCityName()) ? outDo.getCityName() + outDo.getAddress1() : outDo.getAddress1());
                    outputEntity.setAddress1(outDo.getAddress2());
                    outputEntity.setTelPhone(outDo.getPhone());
                    if (outDo.getLogisticsChannelId() == 1 || outDo.getLogisticsChannelId() == 2) {
                        //黑猫小包
                        outputEntity.setTrackName("ネコポス");
                    } else if (outDo.getLogisticsChannelId() == 3) {
                        outputEntity.setTrackName("宅急便");
                    }
                    outputEntity.setCommoditySku(productSkuMap.get(orderOutboundProductDo.getProductId()));
                    outputEntity.setCount(String.valueOf(orderOutboundProductDo.getForecastQty()));
                    exportList.add(outputEntity);
                }
                orderIdMap.put(outDo.getPlatformOrderNo(), outDo.getId());
            }
        }
        if (exportList.isEmpty()) {
            return;
        }
        String jdyToken = null;
        try {
            jdyToken = this.getJdyToken();
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (StringUtils.isBlank(jdyToken)) {
            return;
        }
        SingleResult result = null;
        String errorStr = null;
        try {
            result = this.postToJdy(jdyToken, "api/orderOutput/dealForLabella", exportList);
        } catch (Exception e) {
            errorStr = e.getMessage();
            errorStr = errorStr.substring(0, 80);
        }
        List<OperateRecordDo> operateRecordDos = new ArrayList<>();
        if (result == null) {
            failMap.computeIfAbsent(errorStr, k -> new ArrayList<>()).addAll(orderIdMap.values());
        } else if (!"200".equals(result.getCode())) {
            failMap.computeIfAbsent(result.getMessage(), k -> new ArrayList<>()).addAll(orderIdMap.values());
        } else {
            List<OrderImportErrorVo1> errorVo1s = JSONArray.parseArray(result.getData().toString(), OrderImportErrorVo1.class);
            List<String> errorNos = errorVo1s.stream().filter(e -> !"订单已存在".equals(e.getError())).map(OrderImportErrorVo1::getMsg).distinct().collect(Collectors.toList());
            List<Long> successId = orderIdMap.keySet().stream().filter(k -> !errorNos.contains(k)).map(orderIdMap::get).collect(Collectors.toList());
            if (!successId.isEmpty()) {
                LambdaUpdateWrapper<OrderOutboundDo> updateWrapper = new LambdaUpdateWrapper<>();
                updateWrapper.set(OrderOutboundDo::getSendOut, 2);
                updateWrapper.set(OrderOutboundDo::getStatus, 2);
                updateWrapper.in(OrderOutboundDo::getId, successId);
                updateWrapper.set(OrderOutboundDo::getSendOutFailMsg, null);
                updateWrapper.set(OrderOutboundDo::getExceptionReason, null);
                updateWrapper.set(OrderOutboundDo::getExceptionType, null);
                updateWrapper.set(OrderOutboundDo::getExceptionTime, null);
                this.update(updateWrapper);
                for (Long outId : successId) {
                    OperateRecordDo operateRecordDo = new OperateRecordDo(outId, OperateTypeEnum.OrderOutbound.getValue(), SysConstant.OrderOutbound_PushToJdy,
                            null, new Date(), "");
                    operateRecordDos.add(operateRecordDo);
                }
                progressService.lambdaUpdate()
                        .in(ProgressDo::getBizNumber, successId)
                        .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                        .eq(ProgressDo::getBizType, "推送到三方仓")
                        .set(ProgressDo::getOperateBy, "system")
                        .set(ProgressDo::getOperateTime, DateUtil.date())
                        .update();
            }
            Map<String, List<OrderImportErrorVo1>> errorMap = errorVo1s.stream().collect(Collectors.groupingBy(OrderImportErrorVo1::getError));
            for (String error : errorMap.keySet()) {
                failMap.computeIfAbsent(error, k -> new ArrayList<>()).addAll(errorMap.get(error).stream().map(e -> orderIdMap.get(e.getMsg())).filter(Objects::nonNull).collect(Collectors.toList()));
            }
        }
        for (String s : failMap.keySet()) {
            if ("订单已存在".equals(s)) {
                continue;
            }
            LambdaUpdateWrapper<OrderOutboundDo> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper.set(OrderOutboundDo::getSendOut, 3);
            updateWrapper.set(OrderOutboundDo::getStatus, 6);//异常
            updateWrapper.set(OrderOutboundDo::getSendOutFailMsg, s);
            updateWrapper.set(OrderOutboundDo::getExceptionType, StaticDict.Order_Outbound_ExceptionType.Three.getValue());
            if (s.length() > 100) {
                s = s.substring(0, 100);
            }
            updateWrapper.set(OrderOutboundDo::getExceptionReason, s);
            updateWrapper.set(OrderOutboundDo::getExceptionTime, new Date());
            updateWrapper.in(OrderOutboundDo::getId, failMap.get(s));
            this.update(updateWrapper);
        }
        //操作记录
        if (!operateRecordDos.isEmpty()) {
            List<List<OperateRecordDo>> partition = ListUtil.partition(operateRecordDos, 3000);
            for (List<OperateRecordDo> partList : partition) {
                rabbitTemplate.convertAndSend(RabbitQueueConstant.CrossMall_CMOrder_OperateList, JSON.toJSONString(partList));
            }
        }
    }

    public SingleResult getToJdy(String jdyToken, String jdyUrl) throws Exception {
        //推送到筋斗云
        Map<String, String> headerMap = new HashMap<>();
        headerMap.put("content-type", "application/json; charset=utf-8");
        headerMap.put("token", jdyToken);//C1591:3f82b229-7620-4781-9a06-981eb279cd81
        String serviceUrl = jdyServiceUrl + jdyUrl;
        OkHttpClient client = getUnsafeOkHttpClient();
        OkHttpClient.Builder builder = client.newBuilder();
        builder.connectTimeout(60, TimeUnit.SECONDS)
                .readTimeout(60, TimeUnit.SECONDS)
                .writeTimeout(60, TimeUnit.SECONDS)
                .retryOnConnectionFailure(false);
        Request.Builder requestBuilder = createSPHeader(headerMap);
        Request request = requestBuilder.url(serviceUrl)
                .get()
                .build();
        Response response;
        String body;
        response = builder.build().newCall(request).execute();
        body = response.body().string();
        if (StringUtils.isBlank(body)) {
            return SingleResult.failure("连接超时");
        }
        return JSON.parseObject(body, SingleResult.class);
    }

    @Transactional(noRollbackFor = Exception.class)
    @Override
    public SingleResult postToJdy(String jdyToken, String jdyUrl, Object data) throws Exception {
        //推送到筋斗云
        Map<String, String> headerMap = new HashMap<>();
        headerMap.put("content-type", "application/json; charset=utf-8");
        headerMap.put("token", jdyToken);//C1591:3f82b229-7620-4781-9a06-981eb279cd81
        String serviceUrl = jdyServiceUrl + jdyUrl;
        OkHttpClient client = getUnsafeOkHttpClient();
        OkHttpClient.Builder builder = client.newBuilder();
        builder.connectTimeout(120, TimeUnit.SECONDS)
                .readTimeout(120, TimeUnit.SECONDS)
                .writeTimeout(120, TimeUnit.SECONDS)
                .retryOnConnectionFailure(false);
        Request.Builder requestBuilder = createSPHeader(headerMap);
        Request request = requestBuilder.url(serviceUrl)
                .post(RequestBody.create(MediaType.parse("application/json"), JSON.toJSONString(data)))
                .build();
        Response response = null;
        String body = "";
        response = builder.build().newCall(request).execute();
        body = response.body().string();
        if (StringUtils.isBlank(body)) {
            return SingleResult.failure("连接超时");
        }
        return JSON.parseObject(body, SingleResult.class);
    }


    @Transactional(noRollbackFor = Exception.class)
    @Override
    public String getJdyToken() throws Exception {
        String key = "jdyToken_" + customer;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
            return (String) redisTemplate.opsForValue().get(key);
        }
        Map<String, String> headerMap = new HashMap<>();
        headerMap.put("content-type", "application/json; charset=utf-8");
        String serviceUrl = jdyServiceUrl + "api/open/orderOutput/getTokenLabella?username=donghailongwang&password=258369&customer=" + customer;

        OkHttpClient client = getUnsafeOkHttpClient();
        OkHttpClient.Builder builder = client.newBuilder();
        builder.connectTimeout(60, TimeUnit.SECONDS)
                .readTimeout(60, TimeUnit.SECONDS)
                .writeTimeout(60, TimeUnit.SECONDS)
                .retryOnConnectionFailure(false);
        Request.Builder requestBuilder = createSPHeader(headerMap);
        Request request = requestBuilder.url(serviceUrl)
                .post(RequestBody.create(MediaType.parse("application/json"), ""))
                .build();
        Response response = null;
        String body = "";
//        response = client.newCall(request).execute();
        response = builder.build().newCall(request).execute();
        body = response.body().string();
        if (StringUtils.isBlank(body)) {
            return null;
        }
        SingleResult result = JSON.parseObject(body, SingleResult.class);
        if ("200".equals(result.getCode()) && result.getData() != null) {
            redisTemplate.opsForValue().set(key, result.getData().toString(), 60 * 60 * 9, TimeUnit.SECONDS);
        }
        return result.getData().toString();
    }


    /**
     * 定时获取 jdy的单号
     */
    @Override
    public void getJdyTrackNumber() {
        if (!StaticDict.Company_Type.Labella.getValue().equals(customer) && !StaticDict.Company_Type.Lemooli.getValue().equals(customer)) {
            return;
        }
        //近七天 推送成功状态的订单 去获取单号
        List<OrderOutboundDo> list = baseMapper.selectList(new LambdaQueryWrapper<OrderOutboundDo>().select(OrderOutboundDo::getId, OrderOutboundDo::getPlatformOrderNo, OrderOutboundDo::getCompanyId)
                .eq(OrderOutboundDo::getSendOut, 2)
                .ge(OrderOutboundDo::getCreateTime, DateUtils.dateFormatToString(DateUtils.passingDate(new Date(), -20)))
                .le(OrderOutboundDo::getCreateTime, DateUtils.dateFormatToString(DateUtils.passingDateHour(new Date(), -10))));
        if (list.isEmpty()) {
            return;
        }
        Map<Long, List<OrderOutboundDo>> collect = list.stream().collect(Collectors.groupingBy(OrderOutboundDo::getCompanyId));
        for (Long companyId : collect.keySet()) {
            List<List<OrderOutboundDo>> split = ListUtil.split(collect.get(companyId), 200);
            for (List<OrderOutboundDo> orderOutboundDos : split) {
                rabbitTemplate.convertAndSend(RabbitQueueConstant.OutBound_Labella_Order_GetJDYTrackNumber, JSON.toJSONString(orderOutboundDos));
            }
        }

    }


    @Override
    public void getJdyTrackNumberManual(List<String> platformOrders) {
        List<OrderOutboundDo> list = baseMapper.selectList(new LambdaQueryWrapper<OrderOutboundDo>().select(OrderOutboundDo::getId, OrderOutboundDo::getPlatformOrderNo, OrderOutboundDo::getCompanyId)
                .eq(OrderOutboundDo::getSendOut, 2).in(OrderOutboundDo::getPlatformOrderNo, platformOrders));
        this.consumerGetJdyTrackNumber(list);
    }

    @Override
    public void consumerGetJdyTrackNumber(List<OrderOutboundDo> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<String> platformOrderNos = list.stream().map(OrderOutboundDo::getPlatformOrderNo).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
        if (platformOrderNos.isEmpty()) {
            return;
        }
        String jdyToken = null;
        try {
            jdyToken = this.getJdyToken();
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (StringUtils.isBlank(jdyToken)) {
            log.error("获取jdy单号失败：筋斗云token获取失败");
            return;
        }
        SingleResult result = null;
        try {
            result = this.postToJdy(jdyToken, "api/orderOutput/labellaGetTrackNumber", platformOrderNos);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (result == null) {
            log.error("获取jdy单号失败：筋斗云返回null");
            return;
        } else if (!"200".equals(result.getCode())) {
            log.error("获取jdy单号失败：" + result.getMessage());
            return;
        } else if (result.getData() == null) {
            return;
        }
        List<LabellaOrderTrackNumberDto> trackNumberList = JSONArray.parseArray(result.getData().toString(), LabellaOrderTrackNumberDto.class);
        if (trackNumberList.isEmpty()) {
            return;
        }
        Map<String, String> orderTrackMap = trackNumberList.stream()
                .filter(dto -> StringUtils.isNotBlank(dto.getPlatformNo()) && StringUtils.isNotBlank(dto.getTrackNumber()))
                .collect(Collectors.groupingBy(
                                LabellaOrderTrackNumberDto::getPlatformNo,
                                Collectors.mapping(
                                        LabellaOrderTrackNumberDto::getTrackNumber,
                                        Collectors.collectingAndThen(
                                                Collectors.toCollection(LinkedHashSet::new),  // 保持顺序的去重集合
                                                set -> {
                                                    //一个订单最多保留20个
                                                    List<String> limitedList = new ArrayList<>(set).subList(0, Math.min(set.size(), 20));
                                                    return String.join(",", limitedList); // 转换为字符串
                                                })
                                )
                        )
                );
        Map<Long, String> successMap = list.stream().filter(i -> orderTrackMap.get(i.getPlatformOrderNo()) != null).collect(Collectors.toMap(OrderOutboundDo::getId, OrderOutboundDo::getPlatformOrderNo));
        if (successMap.isEmpty()) {
            return;
        }
        /*//获取一件代发的包裹
        List<OrderOutboundPackageDo> packageList = outboundPackageService.list(Wrappers.<OrderOutboundPackageDo>lambdaQuery()
                .select(OrderOutboundPackageDo::getOutboundId, OrderOutboundPackageDo::getId)
                .in(OrderOutboundPackageDo::getOutboundId, successMap.keySet())
                .eq(OrderOutboundPackageDo::getSerialNumber, 1));
        //去更新
        List<Long> successId = new ArrayList<>();
        for (OrderOutboundPackageDo packageDo : packageList) {
            String trackNumber = orderTrackMap.get(successMap.get(packageDo.getOutboundId())).replaceAll("-", "").trim();
            if (StringUtils.isBlank(trackNumber)) {
                continue;
            }
            outboundPackageService.lambdaUpdate()
                    .eq(OrderOutboundPackageDo::getId, packageDo.getId()).set(OrderOutboundPackageDo::getTrackingNumber, trackNumber).update();
            this.lambdaUpdate().set(OrderOutboundDo::getSendOut, 4).set(OrderOutboundDo::getSendOutFailMsg, null)
                    .set(OrderOutboundDo::getTrackingNumber, trackNumber)
                    .eq(OrderOutboundDo::getId, packageDo.getOutboundId()).update();
            successId.add(packageDo.getOutboundId());
        }*/
        //获取一件代发的包裹
        Map<Long, List<OrderOutboundPackageDo>> outIdMatchPackages = outboundPackageService.list(
                Wrappers.<OrderOutboundPackageDo>lambdaQuery()
                        .select(OrderOutboundPackageDo::getOutboundId, OrderOutboundPackageDo::getId)
                        .in(OrderOutboundPackageDo::getOutboundId, successMap.keySet())
        ).stream().collect(Collectors.groupingBy(OrderOutboundPackageDo::getOutboundId));
        //去更新
        List<Long> successId = new ArrayList<>();
        for (Map.Entry<Long, List<OrderOutboundPackageDo>> entry : outIdMatchPackages.entrySet()) {
            String trackNumber = orderTrackMap.get(successMap.get(entry.getKey())).replaceAll("-", "").trim();
            if (StringUtils.isBlank(trackNumber)) {
                continue;
            }
            String[] trackNoArr = trackNumber.split(",");
            for (int i = 0; i < trackNoArr.length; i++) {
                outboundPackageService.lambdaUpdate()
                        .eq(OrderOutboundPackageDo::getOutboundId, entry.getKey())
                        .eq(OrderOutboundPackageDo::getSerialNumber, i + 1)
                        .set(OrderOutboundPackageDo::getTrackingNumber, trackNoArr[i])
                        .update();
            }
            this.lambdaUpdate().set(OrderOutboundDo::getSendOut, 4).set(OrderOutboundDo::getSendOutFailMsg, null)
                    .set(OrderOutboundDo::getTrackingNumber, trackNumber)
                    .eq(OrderOutboundDo::getId, entry.getKey()).update();
            successId.add(entry.getKey());
        }
        //自动发货扣库存
//        log.error("获取单号后 准备自动发货: " + successId);
        ImportUpdateTrackingNumberAutoSendApiBo autoSendApiBo = ImportUpdateTrackingNumberAutoSendApiBo.builder().successId(successId).orderType(0).build();
        rabbitTemplate.convertAndSend(RabbitQueueConstant.OutBound_Labella_Order_AutoSend, JSON.toJSONString(autoSendApiBo));
    }


    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public List<String> consumerJdyOutboundAutoSendByNo(List<String> outboundNoList, int orderType, Long whId, Long userId, String username) {
        if (CollUtil.isEmpty(outboundNoList) || outboundNoList.size() > 500) {
            return new ArrayList<>();
        }
        Collection<OrderOutboundDo> outboundList = baseMapper.selectList(new LambdaQueryWrapper<OrderOutboundDo>().select(OrderOutboundDo::getId)
                .isNotNull(OrderOutboundDo::getTrackingNumber)
//                        .in(OrderOutboundDo::getStatus, NormalOutboundStatusEnum.one.getValue(), NormalOutboundStatusEnum.two.getValue(), NormalOutboundStatusEnum.three.getValue(), NormalOutboundStatusEnum.four.getValue())
                .eq(whId != null, OrderOutboundDo::getWhId, whId)
                .in(OrderOutboundDo::getOutboundNo, outboundNoList));
        List<String> list = this.consumerJdyOutboundAutoSend(outboundList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()), orderType, whId, userId, username);
        return list;
    }

    /**
     * 获取单号后 自动发货
     *
     * @param outBoundIds 出库单id列表
     * @return
     */
    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public List<String> consumerJdyOutboundAutoSend(List<Long> outBoundIds, int orderType, Long whId, Long userId, String username) {
//        log.error("获取单号后 自动发货: " + outBoundIds);
        if (CollUtil.isEmpty(outBoundIds)) {
            return Collections.emptyList();
        }
        List<Integer> statusList = orderType == 0 ? Arrays.asList(NormalOutboundStatusEnum.one.getValue(), NormalOutboundStatusEnum.two.getValue(), NormalOutboundStatusEnum.three.getValue(), NormalOutboundStatusEnum.four.getValue()) : Arrays.asList(TransferOutboundStatusEnum.one.getValue(), TransferOutboundStatusEnum.two.getValue(), TransferOutboundStatusEnum.three.getValue());
        Collection<OrderOutboundDo> outboundList = baseMapper.selectList(new LambdaQueryWrapper<OrderOutboundDo>().in(OrderOutboundDo::getId, outBoundIds).in(OrderOutboundDo::getStatus, statusList).eq(ObjectUtil.isNotNull(whId), OrderOutboundDo::getWhId, whId));
        if (CollUtil.isEmpty(outboundList)) {
            return Collections.emptyList();
        }
        List<Long> outboundIdList = outboundList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList());
        List<String> outboundNoList = outboundList.stream().map(OrderOutboundDo::getOutboundNo).collect(Collectors.toList());
        //获取产品信息
        List<OrderOutboundProductDo> outboundProductDoListAll = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundIdList)
                .list();
        Map<Long, List<OrderOutboundProductDo>> outboundIdMatchOutboundProduct = outboundProductDoListAll.stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getOutboundId));
        //产品信息
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, outboundProductDoListAll.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toSet()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //锁定的库存扣除
        Set<Long> outboundProductIdList = outboundProductDoListAll.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toSet());
        List<OutboundPickingDo> pickingDoListAll = outboundPickingService.lambdaQuery()
                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                .in(OutboundPickingDo::getOutboundProductId, outboundProductIdList)
                .list();
        Map<Long, List<OutboundPickingDo>> outboundProductIdMatchPick = pickingDoListAll.stream().collect(Collectors.groupingBy(OutboundPickingDo::getOutboundProductId));
        //查询库位信息 用于匹配库存属性
        StorageLocationQueryBo slBo = new StorageLocationQueryBo();
        slBo.setIdList(pickingDoListAll.stream().map(OutboundPickingDo::getRackId).collect(Collectors.toSet()));
        slBo.setTenantId(outboundList.iterator().next().getTenantId());
        slBo.setPageSize(1000);
        Map<Long, StorageLocationVo> cellIdMatchDo = storageLocationService.getListPage(slBo).getData()
                .stream().collect(Collectors.toMap(StorageLocationVo::getId, val -> val));
        List<PlatOrderSendConsumerReq> success = new ArrayList<>();
        Map<OrderOutboundDo, List<OutboundPickingDo>> postHandleMap = new HashMap<>();
        List<SysStockDeductDto> sysStockDeductDtoList = new ArrayList<>();
        for (OrderOutboundDo outboundDo : outboundList) {
            transactionTemplate.execute(status -> {
                try {
                    //修改出库单状态为已发货
                    this.lambdaUpdate()
                            .eq(OrderOutboundDo::getId, outboundDo.getId())
                            .set(OrderOutboundDo::getOutboundTime, DateUtil.date())
                            .set(OrderOutboundDo::getStatus, orderType == 0 ? NormalOutboundStatusEnum.five.getValue() : TransferOutboundStatusEnum.four.getValue())
                            .update();
                    List<OrderOutboundProductDo> outboundProductListByOutboundId = outboundIdMatchOutboundProduct.get(outboundDo.getId());
                    if (CollUtil.isEmpty(outboundProductListByOutboundId)) {
                        return null;
                    }
                    List<OutboundPickingDo> noPickingListByOutboundId = new ArrayList<>();
                    String batchNo = DateUtils.dateFormatToString(outboundDo.getPushWmsTime(), "yyyyMMdd");
                    //批量修改拣货数量，避免波次拣货出现问题
                    outboundProductService.lambdaUpdate()
                            .in(OrderOutboundProductDo::getId, outboundProductListByOutboundId.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                            .setSql("outbound_qty = forecast_qty")
                            .update();
                    for (OrderOutboundProductDo outboundProductDo : outboundProductListByOutboundId) {
                        List<OutboundPickingDo> pickingListByOutboundProductId = outboundProductIdMatchPick.get(outboundProductDo.getId());
                        if (CollUtil.isEmpty(pickingListByOutboundProductId)) {
                            continue;
                        }
                        List<OutboundPickingDo> noPickingList = pickingListByOutboundProductId.stream().filter(val -> Boolean.FALSE.equals(val.getPickFlag())).collect(Collectors.toList());
                        if (CollUtil.isNotEmpty(noPickingList)) {
                            noPickingListByOutboundId.addAll(noPickingList);
                        }
                        //修改拣货状态，避免波次拣货出现问题
                        outboundPickingService.lambdaUpdate()
                                .in(OutboundPickingDo::getId, pickingListByOutboundProductId.stream().map(OutboundPickingDo::getId).collect(Collectors.toList()))
                                .set(OutboundPickingDo::getPickFlag, Boolean.TRUE)
                                .update();

                        for (OutboundPickingDo pickingDo : pickingListByOutboundProductId) {
                            //库存流水实体构建
                            ProductFlowingOutEt productFlowingOutEt = ProductFlowingOutEt.builder()
                                    .sku(productIdMatch.getOrDefault(outboundProductDo.getProductId(), new ProductDo()).getSku())
                                    .companyId(outboundDo.getCompanyId())
                                    .warehouseId(outboundDo.getWhId())
                                    .rackId(pickingDo.getRackId())
                                    .stockAttribute(cellIdMatchDo.getOrDefault(pickingDo.getRackId(), new StorageLocationVo()).getMainType())
                                    .bookQuantity(-pickingDo.getQty())
                                    .orderType(StockOrderTypeEnum.Outbound.getValue())
                                    .noteNo(outboundDo.getOutboundNo())
                                    .innerBatchNo(batchNo)
                                    .tenantId(outboundDo.getTenantId())
                                    .type(StockTypeEnum.one.getValue())
                                    .build();
                            //异步库存扣除
                            sysStockDeductDtoList.add(SysStockDeductDto.builder().pickingDo(pickingDo).productFlowingOutEt(productFlowingOutEt).build());
                        }
                    }
                    postHandleMap.put(outboundDo, noPickingListByOutboundId);
                    if (Objects.equals(outboundDo.getOrderOrigin(), OutboundOriginEnum.three.getValue())
                        && StringUtils.isNotEmpty(outboundDo.getPlatformOrderNo())) {
                        PlatOrderSendConsumerReq req = new PlatOrderSendConsumerReq(Collections.singletonList(
                                new PlatformOrderTrackNoEntity(outboundDo.getId(), outboundDo.getPlatformOrderNo(),
                                        outboundDo.getLogisticsChannelId(), outboundDo.getCarrier(), outboundDo.getTrackingNumber())), 1L, outboundDo.getCompanyId());
                        success.add(req);
                    }
                } catch (Exception e) {
                    // 手动标记回滚
                    status.setRollbackOnly();
                    log.error("获取单号后修改订单状态失败: " + outboundDo.getId(), e);
                }
                return null;
            });
        }
//        log.error("获取单号后 EC订单发货: " + success.size());
        //让监听触发
        transactionTemplate.execute(status -> {
            //批量修改发货状态、日志、进度条等等
            context.publishEvent(new OutboundAutoSendStatusEvent(outboundList, userId, username));
            //出库计费
            context.publishEvent(new OutboundAddCostEvent(outboundList, 0));
            //平台订单出库操作
            context.publishEvent(new PlatformOrderSendEvent(success, 1L));
            //订单出库后修改波次和截单信息
            context.publishEvent(new OutboundPostOperationEvent(postHandleMap));
            //库存异步扣除
            context.publishEvent(new SysStockDeductEvent(sysStockDeductDtoList));
            return null;
        });
        return outboundNoList;
    }

    @Override
    public void autoSendOutboundHandler(Collection<OrderOutboundDo> outboundList, Long userId, String username) {
        if (CollUtil.isEmpty(outboundList)) {
            return;
        }
        List<Long> outboundIdList = outboundList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList());
        //获取进度
        Map<Long, List<ProgressDo>> outboundIdMatchProgressList = progressService.lambdaQuery()
                .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                .in(ProgressDo::getBizNumber, outboundIdList)
                .list().stream().collect(Collectors.groupingBy(ProgressDo::getBizNumber));
        List<OperateRecordDo> operateRecordDos = new ArrayList<>();
        for (OrderOutboundDo outboundDo : outboundList) {
            OperateRecordDo operateRecordDo = new OperateRecordDo();
            operateRecordDo.setContent(Objects.isNull(userId) ? SysConstant.OrderOutbound_Auto_Outbound : SysConstant.OrderOutbound_Confirm_Outbound);
            operateRecordDo.setContentValue("");
            operateRecordDo.setOperateTime(DateUtil.date());
            operateRecordDo.setOperateBy(Objects.isNull(userId) ? 1L : userId);
            operateRecordDo.setOperateUsername(StringUtils.isBlank(username) ? "system" : username);
            operateRecordDo.setType(OperateTypeEnum.OrderOutbound.getValue());
            operateRecordDo.setThirdId(outboundDo.getId());
            operateRecordDo.setDeleteFlag(0);
            operateRecordDos.add(operateRecordDo);
            List<ProgressDo> progressDoList = outboundIdMatchProgressList.get(outboundDo.getId());
            if (CollUtil.isNotEmpty(progressDoList)) {
                for (ProgressDo progressDo : progressDoList.stream().sorted(Comparator.comparing(ProgressDo::getSort)).collect(Collectors.toList())) {
                    if (Objects.isNull(progressDo.getOperateTime())) {
                        progressService.lambdaUpdate()
                                .eq(ProgressDo::getId, progressDo.getId())
                                .set(ProgressDo::getBizType, "自动发货")
                                .set(ProgressDo::getOperateBy, "system")
                                .set(ProgressDo::getOperateTime, DateUtil.date())
                                .update();
                        break;
                    }
                }
                progressService.remove(
                        Wrappers.<ProgressDo>lambdaQuery()
                                .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                                .eq(ProgressDo::getBizNumber, outboundDo.getId())
                                .isNull(ProgressDo::getOperateTime)
                );
            }
        }
        if (CollUtil.isNotEmpty(operateRecordDos)) {
            operateRecordService.insertBatchMy(operateRecordDos);
        }
    }

    //ec出库状态已出库 但是Ec订单未出库

    @Override
    public void dealError(List<Long> outBoundIds, Long whId) {
        if (CollUtil.isEmpty(outBoundIds)) {
            return;
        }
        List<List<Long>> partition = ListUtil.partition(outBoundIds, 1000);
        for (List<Long> longs : partition) {
            Collection<OrderOutboundDo> outboundList = baseMapper.selectList(new LambdaQueryWrapper<OrderOutboundDo>().in(OrderOutboundDo::getId, longs).in(OrderOutboundDo::getStatus, NormalOutboundStatusEnum.one.getValue(), NormalOutboundStatusEnum.two.getValue(), NormalOutboundStatusEnum.three.getValue(), NormalOutboundStatusEnum.four.getValue()).eq(ObjectUtil.isNotNull(whId), OrderOutboundDo::getWhId, whId));
            if (CollUtil.isEmpty(outboundList)) {
                return;
            }
            List<PlatOrderSendConsumerReq> success = new ArrayList<>();
            for (OrderOutboundDo outboundDo : outboundList) {
                if (Objects.equals(outboundDo.getOrderOrigin(), OutboundOriginEnum.three.getValue())
                    && StringUtils.isNotEmpty(outboundDo.getPlatformOrderNo())) {
                    PlatOrderSendConsumerReq req = new PlatOrderSendConsumerReq(Collections.singletonList(
                            new PlatformOrderTrackNoEntity(outboundDo.getId(), outboundDo.getPlatformOrderNo(),
                                    outboundDo.getLogisticsChannelId(), outboundDo.getCarrier(), outboundDo.getTrackingNumber())), 1L, outboundDo.getCompanyId());
                    success.add(req);
                }
            }
            context.publishEvent(new PlatformOrderSendEvent(success, 1L));
        }
    }


    @Override
    public void postOutboundOperation(OrderOutboundDo outboundDo, List<OutboundPickingDo> noPickingListByOutboundId) {
        if (Objects.nonNull(outboundDo.getWaveId())) {
            //修改所属波次出库订单数
            waveService.lambdaUpdate()
                    .eq(OutboundWaveDo::getId, outboundDo.getWaveId())
                    .setSql("outbound_order = outbound_order + 1")
                    .setSql(CollUtil.isNotEmpty(noPickingListByOutboundId), "picking_qty = picking_qty + " + (CollUtil.isNotEmpty(noPickingListByOutboundId) ? noPickingListByOutboundId.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum) : 0))
                    .update();
            // 修改波次的拣货状态
            OutboundWaveDo waveDo = waveService.getById(outboundDo.getWaveId());
            if (waveDo.getOutboundOrder().equals(waveDo.getOrderNum())) {
                waveService.lambdaUpdate()
                        .eq(OutboundWaveDo::getId, outboundDo.getWaveId())
                        .set(OutboundWaveDo::getOutbound, Boolean.TRUE)
                        .update();
            }
            if (CollUtil.isNotEmpty(noPickingListByOutboundId)) {
                boolean qtyEquals = waveDo.getPickingQty().equals(waveDo.getShouldPickingQty());
                if (!(!qtyEquals && waveDo.getStatus().equals(1))) {
                    waveService.lambdaUpdate()
                            .eq(OutboundWaveDo::getId, outboundDo.getWaveId())
                            .set(OutboundWaveDo::getStatus, qtyEquals ? 2 : 1)
                            .update();
                }
            }
        }
        //忽略进行中的截单，直接出库
        boolean isCancel = cancelService.lambdaUpdate()
                .eq(OutboundCancelDo::getOrderKind, OutboundOrderKind.outbound.getValue())
                .eq(OutboundCancelDo::getOutboundId, outboundDo.getId())
                .eq(OutboundCancelDo::getStatus, 0)
                .set(OutboundCancelDo::getStatus, 2)
                .set(OutboundCancelDo::getFinishTime, DateUtil.date())
                .update();
        if (isCancel) {
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.Order_Interception_Fail,
                    "",
                    outboundDo.getId(),
                    1L,
                    "system",
                    OperateTypeEnum.OrderOutbound
            );
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, outboundDo.getId())
                    .set(OrderOutboundDo::getInterceptionFlag, Boolean.FALSE)
                    .update();
        }
    }

    /**
     * 用于stream流的filter方法做实体类的某个属性去重
     */
    static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
        Map<Object, Boolean> seen = new ConcurrentHashMap<>();
        //putIfAbsent方法添加键值对，如果map集合中没有该key对应的值，则直接添加，并返回null，如果已经存在对应的值，则依旧为原来的值。
        //如果返回null表示添加数据成功(不重复)，不重复(null==null :TRUE)
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long normalSave(NormalOrderOutboundSaveBo bo, Long userId, String username, Long companyId, Long tenantId, OutboundOriginEnum origin) throws BizException {
        OrderOutboundDo entityDo = Objects.nonNull(bo.getId()) ? this.getById(bo.getId()) : new OrderOutboundDo();
        if (NormalOutboundStatusEnum.one.getValue().equals(entityDo.getStatus())) {
            return entityDo.getId();
        }
//        BeanUtils.copyProperties(bo, entityDo);
        BeanConvert.INSTANCE.normalOutboundSaveBoToOutboundDo(bo, entityDo);
        if (Objects.isNull(bo.getId())) {
            entityDo.setOutboundNo(this.generateOutboundNo("OBS"));
        }
        List<NormalOrderOutboundSaveBo.NormalOrderOutboundProductSaveBo> productInfoList = bo.getProductInfoList();
        entityDo.setOrderType(OutboundTypeEnum.one.getValue());
        if (productInfoList.size() == 1) {
            Integer forecastQty = productInfoList.get(0).getForecastQty();
            if (forecastQty > 1) {
                entityDo.setOrderVarietyType(OutboundVarietyTypeEnum.two.getValue());
            } else {
                entityDo.setOrderVarietyType(OutboundVarietyTypeEnum.one.getValue());
            }
        } else {
            entityDo.setOrderVarietyType(OutboundVarietyTypeEnum.three.getValue());
        }
        if (entityDo.getSendOut() == null) {
            WarehouseDo warehouseDo = warehouseService.getById(bo.getWhId());
            if (warehouseDo != null && warehouseDo.getWarehouseType() == 1 && "DL01".equals(warehouseDo.getWhCode())) {
                entityDo.setSendOut(1);
            } else {
                entityDo.setSendOut(0);
            }
        }
        entityDo.setOrderOrigin(origin.getValue());
        //只要是手动新建的 不管是不是复制 都是手工单，如果是转草稿 提交的 还是按以前的订单来源
        if (bo.getId() != null && bo.getOrderOrigin() != null) {
            entityDo.setOrderOrigin(bo.getOrderOrigin());
        }
        if (StringUtils.isNotBlank(entityDo.getReceiver())) {
            if (entityDo.getReceiver().length() > 15) {
                entityDo.setReceiver(entityDo.getReceiver().substring(0, 15));
            }
        }
        entityDo.setCompanyId(companyId);
        entityDo.setTenantId(tenantId);
        if (StrUtil.isEmpty(bo.getTrackingNumber())) {
            entityDo.setTrackingNumber(null);
        }
        if (StrUtil.isEmpty(bo.getAppendixIds())) {
            entityDo.setAppendixIds(null);
        }
        if (StrUtil.isEmpty(bo.getDeliveryDate())) {
            entityDo.setDeliveryDate("");
        }
        if (StrUtil.isEmpty(bo.getDeliveryTimeName())) {
            entityDo.setDeliveryTimeName("");
        }
        entityDo.setSignatureService(OutboundSignServiceEnum.getCodeByDesc(bo.getSignatureService()));
        String skuDetail = productInfoList.stream().map(item -> item.getSku() + "*" + item.getForecastQty()).collect(Collectors.joining(","));
        entityDo.setSkuDetail(skuDetail);
        //新增或修改出库单基础数据
        this.saveOrUpdate(entityDo);
        if (Objects.isNull(bo.getId())) {
            //新增创建出库单进度
            this.createNormalProgress(entityDo, username);
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.Create_Outbound_Order,
                    "",
                    entityDo.getId(),
                    userId,
                    username,
                    OperateTypeEnum.OrderOutbound
            );
        } else {
            //修改前删除原始记录
            this.deleteOriginalDataByOutboundId(bo.getId());
            progressService.deleteByIds(Collections.singletonList(bo.getId()), ProgressEnum.OUTBOUND.getTypeCode());
        }
        //操作产品子表
        List<OrderOutboundProductDo> outboundProductDoList = JSONUtil.toList(
                JSONUtil.toJsonStr(productInfoList),
                OrderOutboundProductDo.class
        );
        for (OrderOutboundProductDo orderOutboundProductDo : outboundProductDoList) {
            orderOutboundProductDo.setOutboundId(entityDo.getId());
            orderOutboundProductDo.setOutboundQty(0);
        }
        outboundProductService.saveBatch(outboundProductDoList);

        //为计算包裹尺寸组装数据
        Map<Long, Integer> productIdMatchNum = productInfoList.stream().collect(
                Collectors.toMap(
                        NormalOrderOutboundSaveBo.NormalOrderOutboundProductSaveBo::getProductId,
                        NormalOrderOutboundSaveBo.NormalOrderOutboundProductSaveBo::getForecastQty,
                        Integer::sum
                )
        );
        List<ProductDo> productDoList = productService.lambdaQuery()
                .in(ProductDo::getId, productIdMatchNum.keySet())
                .list();
        Map<Long, String> skuMap = productDoList.stream().collect(Collectors.toMap(ProductDo::getId, ProductDo::getSku));
        List<PackageSizeVo> packageSizeVoList = new ArrayList<>();
        productDoList.forEach(productDo ->
                packageSizeVoList.add(
                        PackageSizeVo.builder()
                                .productId(productDo.getId())
                                .sku(productDo.getSku())
                                .qty(productIdMatchNum.get(productDo.getId()))
                                .length(productDo.getWmsLength() == null ? productDo.getLength() : productDo.getWmsLength())
                                .width(productDo.getWmsWidth() == null ? productDo.getWidth() : productDo.getWmsWidth())
                                .height(productDo.getWmsHeight() == null ? productDo.getHeight() : productDo.getWmsHeight())
                                .weight(productDo.getWmsWeight() == null ? productDo.getWeight() : productDo.getWmsWeight())
                                .build()
                )
        );
        this.calculatePackageSize(entityDo.getId(), 1, entityDo.getTrackingNumber(), entityDo.getLabelFileId(), packageSizeVoList, true);

        if (StrUtil.isNotBlank(entityDo.getAppendixIds())) {
            //上传到正式目录
            fileService.uploadFileMult(entityDo.getAppendixIds().split(","));
        }
        if (NormalOutboundStatusEnum.one.getValue().equals(entityDo.getStatus())) {
            //提交出库单后置操作
            this.pushOrderOutboundToWmsAfterHandler(entityDo, tenantId, userId, username);
            //提交一件代发出库单后续锁定库存和下单校验操作
            this.submitNormalOutboundToWmsAfterLockHandler(entityDo, tenantId, outboundProductDoList);
        }

        //如果不是平台拉单 则需要生成包裹明细
        List<List<ProductMemoMsg>> packageList = new ArrayList<>();
        //自动选取物流渠道的获取一下运费试算得到的信息
        if (Objects.isNull(entityDo.getLogisticsChannelId()) || entityDo.getLogisticsChannelId() <= 0) {
            entityDo = this.getById(entityDo.getId());
        }
        if (!OutboundOriginEnum.three.equals(origin) && entityDo.getLogisticsChannelId() != 3) {
            //oms端手动操作及非黑猫宅急便的订单 需要分包裹
            OrderPackageRes packageRes = this.getPackageList(outboundProductDoList, skuMap);
            packageList = packageRes.getPackageList();
        }
        if (packageList.isEmpty()) {
            //如果黑猫宅急便 则默认一个包裹

        } else {
            List<PlatformOrderPackageDetailDo> oneOrderPackDetailDoList = platformOrderPackageDetailService.getOneOrderPackDetailDoList(packageList, entityDo.getId(), entityDo.getPlatformOrderNo(), null, userId);
            //分包裹
            //分包后置处理
            this.dealPackageDetail(new OrderSubcontractEvent(entityDo.getPlatformOrderNo(), entityDo.getId(), oneOrderPackDetailDoList));
        }

        //如果开启了 前置计费
        OmpSystemEntity ompSet = systemSetService.getOmpSet(tenantId);
        if (ompSet.getOmp().getPreBilling() == 1 && (ompSet.getOmp().getBindCompany() == 0 || ompSet.getOmp().getCompany().contains(entityDo.getCompanyId()))) {
            //预扣
            context.publishEvent(new OutboundAddCostEvent(entityDo, 1));
        }
        return entityDo.getId();
    }


    public void dealPackageDetail(OrderSubcontractEvent event) {
        Long outboundId = event.getOutId();
        List<PlatformOrderPackageDetailDo> platformOrderPackageDetailDoList = event.getPlatformOrderPackageDetailDoList();
        if (CollUtil.isEmpty(platformOrderPackageDetailDoList)) {
            return;
        }
        Map<Long, ProductDo> productIdMatchDo = productService.lambdaQuery()
                .in(ProductDo::getId, platformOrderPackageDetailDoList.stream().map(PlatformOrderPackageDetailDo::getProductId).collect(Collectors.toSet()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //根据包裹序号分数据
        Map<Integer, List<PlatformOrderPackageDetailDo>> packageListBySeq = platformOrderPackageDetailDoList.stream().collect(Collectors.groupingBy(PlatformOrderPackageDetailDo::getLine));
        //删除原有的包裹信息
        outboundPackageDetailService.remove(
                Wrappers.<OrderOutboundPackageDetailDo>lambdaQuery().eq(OrderOutboundPackageDetailDo::getOutboundId, outboundId));
        outboundPackageService.remove(
                Wrappers.<OrderOutboundPackageDo>lambdaQuery().eq(OrderOutboundPackageDo::getOutboundId, outboundId));
        int packNum = 0;
        for (Map.Entry<Integer, List<PlatformOrderPackageDetailDo>> entry : packageListBySeq.entrySet()) {
            Integer serialNumber = entry.getKey();
            if (serialNumber > packNum) {
                packNum = serialNumber;
            }
            List<PlatformOrderPackageDetailDo> value = entry.getValue();
            List<PackageSizeVo> packageSizeVoList = new ArrayList<>();
            value.forEach(item -> {
                        ProductDo productDo = productIdMatchDo.get(item.getProductId());
                        if (productDo == null) {
                            return;
                        }
                        packageSizeVoList.add(
                                PackageSizeVo.builder()
                                        .productId(item.getProductId())
                                        .sku(item.getProductSkuCode())
                                        .itemMemo(item.getItemMemo())
                                        .qty(item.getCount())
                                        .length(productDo.getWmsLength() == null ? productDo.getLength() : productDo.getWmsLength())
                                        .width(productDo.getWmsWidth() == null ? productDo.getWidth() : productDo.getWmsWidth())
                                        .height(productDo.getWmsHeight() == null ? productDo.getHeight() : productDo.getWmsHeight())
                                        .weight(productDo.getWmsWeight() == null ? productDo.getWeight() : productDo.getWmsWeight())
                                        .build()
                        );
                    }
            );
            //根据产品列表计算包裹尺寸
            this.calculatePackageSize(outboundId, serialNumber, StrUtil.isNotBlank(value.get(0).getTrackNumber()) ? value.get(0).getTrackNumber() : null, null, packageSizeVoList, true);
        }
        String trackNumberStr = platformOrderPackageDetailDoList.stream()
                .map(PlatformOrderPackageDetailDo::getTrackNumber)
                .filter(StrUtil::isNotBlank)
                .distinct()
                .collect(Collectors.joining(","));
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, outboundId)
                .set(StrUtil.isNotBlank(trackNumberStr), OrderOutboundDo::getTrackingNumber, trackNumberStr)
                .set(OrderOutboundDo::getPackageNum, packNum)
                .update();
    }


    public OrderPackageRes getPackageList(List<OrderOutboundProductDo> itemList, Map<Long, String> skuMap) {
        //分包逻辑
        List<List<ProductMemoMsg>> packageList = new ArrayList<>();
        boolean zhaiFlag = false;
        List<Long> productIds = itemList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList());
        Map<Long, ProductMinSizeDto> sizeMap = new HashMap<>();
        if (!productIds.isEmpty()) {
            sizeMap = productDao.getProductMinSize(productIds).stream().collect(Collectors.toMap(ProductMinSizeDto::getProductId, i -> i));
        }
        List<ProductMemoMsg> less30List = new ArrayList<>();
        for (OrderOutboundProductDo item : itemList) {
            ProductMinSizeDto sizeB = sizeMap.get(item.getProductId());
            if (sizeB == null) {
                continue;
            }
            if (sizeB.getMinSize().compareTo(new BigDecimal("3")) > 0) {
                zhaiFlag = true;
                break;
            }
            for (int i = 0; i < item.getForecastQty(); i++) {
                less30List.add(new ProductMemoMsg(skuMap.get(item.getProductId()), item.getProductId(), sizeB.getLength(), sizeB.getWidth(),
                        sizeB.getHeight(), sizeB.getWeight(), sizeB.getLengthBs(), sizeB.getWidthBs(), sizeB.getHeightBs(), sizeB.getWeightBs(), sizeB.getMinSize()));
            }
        }
        if (!zhaiFlag) {
            this.sort(less30List);
            this.getPackage(packageList, less30List);
        }
        return new OrderPackageRes(packageList, zhaiFlag);
    }

    public void getPackage(List<List<ProductMemoMsg>> packageList, List<ProductMemoMsg> less30List) {
        if (CollUtil.isEmpty(less30List)) {
            return;
        }
        List<ProductMemoMsg> list = new ArrayList<>();
        BigDecimal sizeBig = new BigDecimal(0);
        for (int i = 0; i < less30List.size(); i++) {
            ProductMemoMsg et = less30List.get(i);
            BigDecimal itemMemo = et.getItemMemo();
            if (itemMemo.compareTo(BigDecimal.valueOf(3d)) > 0) {
                packageList.add(Collections.singletonList(et));
                less30List.remove(i);
                i--;
                continue;
            }
            BigDecimal add = sizeBig.add(itemMemo);
            if (add.compareTo(new BigDecimal("3")) > 0) {
                //大于
                break;
            } else {
                sizeBig = add;
                list.add(et);
                less30List.remove(i);
                i--;
            }
        }
        packageList.add(list);
        if (!CollUtil.isEmpty(less30List)) {
            this.getPackage(packageList, less30List);
        }
    }


    public void sort(List<ProductMemoMsg> list) {
        list.sort(
                (o1, o2) -> {
                    if (o1.getItemMemo().compareTo(o2.getItemMemo()) == 0) {
                        return o1.getProductSkuCode().compareTo(o2.getProductSkuCode());
                    } else if (o1.getItemMemo().compareTo(o2.getItemMemo()) > 0) {
                        return -1;
                    } else {
                        return 1;
                    }
                }
        );
    }

    /**
     * 计算包裹尺寸
     *
     * @param outboundId        出库单id
     * @param serialNumber      包裹序号
     * @param trackingNumber    物流跟踪号
     * @param labelFileId       面单
     * @param packageSizeVoList 尺寸列表
     */
    @Override
    public OrderOutboundPackageDo calculatePackageSize(Long outboundId, Integer serialNumber, String trackingNumber, Long labelFileId, List<PackageSizeVo> packageSizeVoList, boolean save) {
        OrderOutboundPackageDo packageDo = new OrderOutboundPackageDo();
        packageDo.setOutboundId(outboundId);
        packageSizeVoList.stream().map(PackageSizeVo::getLength).max(BigDecimal::compareTo).ifPresent(item -> {
            packageDo.setLength(item);
            packageDo.setLengthBs(item.divide(BigDecimal.valueOf(2.54), 3, RoundingMode.HALF_UP));
        });
        packageSizeVoList.stream().map(PackageSizeVo::getWidth).max(BigDecimal::compareTo).ifPresent(item -> {
            packageDo.setWidth(item);
            packageDo.setWidthBs(item.divide(BigDecimal.valueOf(2.54), 3, RoundingMode.HALF_UP));
        });
        //所有相加
        BigDecimal height = packageSizeVoList.stream().map(i -> i.getHeight().multiply(BigDecimal.valueOf(i.getQty()))).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal weight = packageSizeVoList.stream().map(i -> i.getWeight().multiply(BigDecimal.valueOf(i.getQty()))).reduce(BigDecimal.ZERO, BigDecimal::add);
        packageDo.setHeight(height);
        packageDo.setHeightBs(height.divide(BigDecimal.valueOf(2.54), 3, RoundingMode.HALF_UP));
        packageDo.setWeight(weight);
        packageDo.setWeightBs(weight.multiply(BigDecimal.valueOf(2.2046)).setScale(3, RoundingMode.HALF_UP));
        packageDo.setSerialNumber(serialNumber);
        if (StrUtil.isNotBlank(trackingNumber)) {
            packageDo.setTrackingNumber(trackingNumber);
        }
        if (Objects.nonNull(labelFileId)) {
            packageDo.setLabelFileId(labelFileId);
        }
        if (save) {
            outboundPackageService.save(packageDo);
        }
        List<OrderOutboundPackageDetailDo> packageDetailDoList = JSONUtil.toList(JSONUtil.toJsonStr(packageSizeVoList), OrderOutboundPackageDetailDo.class);
        if (CollUtil.isNotEmpty(packageDetailDoList) && save) {
            packageDetailDoList.forEach(item -> {
                item.setPackageId(packageDo.getId());
                item.setOutboundId(outboundId);
            });
            outboundPackageDetailService.saveBatch(packageDetailDoList);
        }
        return packageDo;
    }

    /**
     * 提交一件代发出库单后续锁定库存和下单校验操作
     *
     * @return 返回问题，null为成功，其他为失败
     */
    public String[] submitNormalOutboundToWmsAfterLockHandler(OrderOutboundDo entityDo, Long tenantId, List<OrderOutboundProductDo> outboundProductDoList) {
        LogisticsChannelDo channelDo = logisticsChannelService.getById(entityDo.getLogisticsChannelId());
        boolean isCustomChannel = Objects.nonNull(channelDo) && Objects.equals(channelDo.getChannelCode(), "custom_channel");
        List<TrialFreightRes> trialFreightResList = new ArrayList<>();
        //自定义渠道不下单校验
        if (!isCustomChannel) {
            //运费试算(下单校验)
            trialFreightResList = quotationSchemeService.omsTrialFreightFee(this.buildTrialFreightReq(entityDo));
            if (CollUtil.isEmpty(trialFreightResList)) {
                this.lambdaUpdate()
                        .eq(OrderOutboundDo::getId, entityDo.getId())
                        .set(OrderOutboundDo::getExceptionTime, DateUtil.date())
                        .set(OrderOutboundDo::getStatus, NormalOutboundStatusEnum.six.getValue())
                        .set(OrderOutboundDo::getExceptionType, StringsUtil.createI18Message(SysConstant.Freight_Trial_Failed))
                        .set(OrderOutboundDo::getExceptionReason, StringsUtil.createI18Message(SysConstant.Freight_Trial_Failed))
                        .update();
                return new String[]{SysConstant.Freight_Trial_Failed};
            }
        }
        if (isCustomChannel || trialFreightResList.get(0).isPlaceOrderFlag()) {
            if (Objects.equals(entityDo.getLogisticsChannelId(), -1L)) {
                //自动选取最低价的物流渠道
                TrialFreightRes trialFreightRes = trialFreightResList.get(0);
                LogisticsChannelDo newChannelDo = logisticsChannelService.getById(trialFreightRes.getChannelId());
                this.lambdaUpdate()
                        .eq(OrderOutboundDo::getId, entityDo.getId())
                        .set(OrderOutboundDo::getLogisticsChannelId, newChannelDo.getId())
                        .set(OrderOutboundDo::getCarrier, newChannelDo.getCarrierCode())
                        .update();
            }
            try {
                //锁定库存
                for (OrderOutboundProductDo item : outboundProductDoList) {
                    //todo 库位优先级排序 ??
                    List<ProductStockRackDo> productStockRackDoList = productStockRackService.getFirstInFirstOutBySku(
                            tenantId,
                            entityDo.getWhId(),
                            item.getProductId(),
                            item.getForecastQty(),
                            null
                    );
                    if (CollUtil.isEmpty(productStockRackDoList)) {
                        throw new BizException(SysConstant.Inventory_Shortage, item.getProductId());
                    }
                    //需要锁定的数量
                    AtomicInteger needLock = new AtomicInteger(item.getForecastQty());
                    //锁定库存并保存拣货记录
                    this.lockStockAndSavePicking(
                            productStockRackDoList,
                            needLock,
                            String.valueOf(item.getProductId()),
                            item.getId(),
                            Boolean.FALSE
                    );
                }
            } catch (Exception e) {
                // 回退部分库存锁定
                if (CollUtil.isNotEmpty(outboundProductDoList)) {
                    List<OutboundPickingDo> pickingDoList = outboundPickingService.lambdaQuery()
                            .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                            .in(OutboundPickingDo::getOutboundProductId, outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                            .list();
                    if (CollUtil.isNotEmpty(pickingDoList)) {
                        this.unLock(
                                JSONUtil.toList(JSONUtil.toJsonStr(pickingDoList), OutboundPickingDo.class),
                                pickingDoList,
                                new AtomicInteger(pickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum))
                        );
                    }
                }
                //下单失败，不锁定库存，异常处理
                String s = e.getMessage();
                if (s.length() > 100) {
                    s = s.substring(0, 100);
                }
                this.lambdaUpdate()
                        .eq(OrderOutboundDo::getId, entityDo.getId())
                        .set(OrderOutboundDo::getMarkExceptionTime, null)
                        .set(OrderOutboundDo::getExceptionTime, DateUtil.date())
                        .set(OrderOutboundDo::getStatus, NormalOutboundStatusEnum.six.getValue())
                        .set(OrderOutboundDo::getExceptionType, StringsUtil.createI18Message(StaticDict.Order_Outbound_ExceptionType.One.getValue()))
                        .set(OrderOutboundDo::getExceptionReason, s)
                        .update();
                return new String[]{SysConstant.Wms_Stock_Shortage, StringsUtil.createI18Message(e.getMessage())};
            }
        } else {
            //下单失败，不锁定库存，异常处理
            String s = trialFreightResList.get(0).getPlaceOrderDetailMsg();
            if (s.length() > 100) {
                s = s.substring(0, 100);
            }
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, entityDo.getId())
                    .set(OrderOutboundDo::getExceptionTime, DateUtil.date())
                    .set(OrderOutboundDo::getStatus, NormalOutboundStatusEnum.six.getValue())
                    .set(OrderOutboundDo::getExceptionType, trialFreightResList.get(0).getPlaceOrderMsg())
                    .set(OrderOutboundDo::getExceptionReason, s)
                    .update();
            return new String[]{trialFreightResList.get(0).getPlaceOrderMsg(), trialFreightResList.get(0).getPlaceOrderDetailMsg()};
        }
        return null;
    }

    @Override
    public void transferSave(TransferOrderOutboundSaveBo bo, Long userId, String username, Long tenantId, Long companyId) throws BizException {
        OrderOutboundDo entityDo = Objects.nonNull(bo.getId()) ? this.getById(bo.getId()) : new OrderOutboundDo();
        if (TransferOutboundStatusEnum.one.getValue().equals(entityDo.getStatus())) {
            return;
        }
//        BeanUtils.copyProperties(bo, entityDo);
        BeanConvert.INSTANCE.transferOutboundSaveBoToOutboundDo(bo, entityDo);
        if (Objects.isNull(bo.getId())) {
            entityDo.setOutboundNo(this.generateOutboundNo("OBC"));
        }
        List<TransferOrderOutboundProductSaveBo> productInfoList = bo.getProductInfoList();
        entityDo.setOrderType(OutboundTypeEnum.two.getValue());
        entityDo.setCompanyId(companyId);
        entityDo.setInsuranceFlag(Boolean.FALSE);
        if (StrUtil.isEmpty(bo.getDeliveryDate())) {
            entityDo.setDeliveryDate("");
        }
        if (StrUtil.isEmpty(bo.getDeliveryTimeName())) {
            entityDo.setDeliveryTimeName("");
        }
        Integer outboundMode = entityDo.getOutboundMode();
        String skuDetail = "";
        if (outboundMode.equals(1)) {
            skuDetail = productInfoList.stream().map(item -> item.getSku() + "*" + item.getForecastQty()).collect(Collectors.joining(","));
        } else if (outboundMode.equals(2)) {
            skuDetail = productInfoList.stream().map(item -> item.getBoxTypeNumber() + "*" + item.getForecastBoxQty()).distinct().collect(Collectors.joining(","));
        }
        entityDo.setSkuDetail(skuDetail);
        //新增或修改出库单基础数据
        this.saveOrUpdate(entityDo);
        if (Objects.isNull(bo.getId())) {
            //新增创建出库单进度
            this.createTransferProgress(entityDo, username);
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.Create_Outbound_Order,
                    "",
                    entityDo.getId(),
                    userId,
                    username,
                    OperateTypeEnum.OrderOutbound
            );
        } else {
            //修改前删除原始记录
            this.deleteOriginalDataByOutboundId(bo.getId());
        }
        //操作产品子表
        List<OrderOutboundProductDo> outboundProductDoList = JSONUtil.toList(
                JSONUtil.toJsonStr(productInfoList),
                OrderOutboundProductDo.class
        );
        outboundProductDoList.forEach(orderOutboundProductDo -> orderOutboundProductDo.setOutboundId(entityDo.getId()));
        outboundProductService.saveBatch(outboundProductDoList);
        List<PackageSizeVo> packageSizeVoList = new ArrayList<>();
        if (outboundMode.equals(1)) {
            //为计算包裹尺寸组装数据
            Map<Long, Integer> productIdMatchNum = productInfoList.stream().collect(
                    Collectors.toMap(
                            TransferOrderOutboundProductSaveBo::getProductId,
                            TransferOrderOutboundProductSaveBo::getForecastQty)
            );
            productService.lambdaQuery()
                    .in(ProductDo::getId, productIdMatchNum.keySet())
                    .list()
                    .forEach(productDo ->
                            packageSizeVoList.add(
                                    PackageSizeVo.builder()
                                            .productId(productDo.getId())
                                            .sku(productDo.getSku())
                                            .qty(productIdMatchNum.get(productDo.getId()))
                                            .length(productDo.getWmsLength() == null ? productDo.getLength() : productDo.getWmsLength())
                                            .width(productDo.getWmsWidth() == null ? productDo.getWidth() : productDo.getWmsWidth())
                                            .height(productDo.getWmsHeight() == null ? productDo.getHeight() : productDo.getWmsHeight())
                                            .weight(productDo.getWmsWeight() == null ? productDo.getWeight() : productDo.getWmsWeight())
                                            .build()
                            )
                    );
        } else if (outboundMode.equals(2)) {
            Map<String, Integer> boxTypeNumberMatchNum = productInfoList.stream().collect(
                    Collectors.toMap(
                            TransferOrderOutboundProductSaveBo::getBoxTypeNumber,
                            TransferOrderOutboundProductSaveBo::getForecastBoxQty,
                            (o1, o2) -> o1
                    )
            );
            boxService.lambdaQuery()
                    .in(OrderBoxDo::getBoxTypeNumber, boxTypeNumberMatchNum.keySet())
                    .groupBy(OrderBoxDo::getBoxTypeNumber)
                    .list().forEach(boxDo ->
                            packageSizeVoList.add(
                                    PackageSizeVo.builder()
                                            .productId(-1L)
                                            .sku(boxDo.getBoxTypeNumber())
                                            .qty(boxTypeNumberMatchNum.get(boxDo.getBoxTypeNumber()))
                                            .length(boxDo.getLength())
                                            .width(boxDo.getWidth())
                                            .height(boxDo.getHeight())
                                            .weight(boxDo.getWeight())
                                            .build()
                            )
                    );
        }
        //根据产品列表计算包裹尺寸
        this.calculatePackageSize(entityDo.getId(), 1, entityDo.getTrackingNumber(), entityDo.getLabelFileId(), packageSizeVoList, true);
        if (StrUtil.isNotBlank(entityDo.getAppendixIds())) {
            //上传到正式目录
            fileService.uploadFileMult(entityDo.getAppendixIds().split(","));
        }
        if (TransferOutboundStatusEnum.one.getValue().equals(entityDo.getStatus())) {
            //提交出库单后置操作
            this.pushOrderOutboundToWmsAfterHandler(entityDo, tenantId, userId, username);
            //提交备货中转出库单后续锁定库存和下单校验操作
            String[] failMsg = this.submitTransferOutboundToWmsAfterLockHandler(entityDo, tenantId, outboundProductDoList);
            if (ArrayUtil.isEmpty(failMsg)) {
                CompletableFuture.runAsync(() -> {
                    //提交时生成纳品书放于附件字段
                    ByteArrayOutputStream pdfOutputStream = outboundImportExportService.getDeliveryOrderPdfOutputStream(entityDo.getId());
                    if (pdfOutputStream != null) {
                        Long deliveryOrderId = fileService.uploadFileToFormalPath(new ByteArrayInputStream(pdfOutputStream.toByteArray()), entityDo.getOutboundNo() + "納品書.pdf");
                        if (deliveryOrderId != null && deliveryOrderId > 0) {
                            this.lambdaUpdate()
                                    .eq(OrderOutboundDo::getId, entityDo.getId())
                                    .set(OrderOutboundDo::getAppendixIds, StrUtil.isNotBlank(entityDo.getAppendixIds()) ? entityDo.getAppendixIds() + "," + deliveryOrderId : String.valueOf(deliveryOrderId))
                                    .update();
                        }
                    }
                }, threadPoolExecutor);
            }
        }
        //如果开启了 前置计费
        OmpSystemEntity ompSet = systemSetService.getOmpSet(tenantId);
        if (ompSet.getOmp().getPreBilling() == 1 && (ompSet.getOmp().getBindCompany() == 0 || ompSet.getOmp().getCompany().contains(entityDo.getCompanyId()))) {
//            rabbitTemplate.convertAndSend(RabbitQueueConstant.Outbound_Estimate_Cost, JSON.toJSONString(entityDo));
            context.publishEvent(new OutboundAddCostEvent(entityDo, 1));
        }
    }

    /**
     * 提交备货中转出库单后续锁定库存和下单校验操作
     *
     * @return 返回错误信息，null表示成功
     */
    public String[] submitTransferOutboundToWmsAfterLockHandler(OrderOutboundDo entityDo, Long tenantId, List<OrderOutboundProductDo> outboundProductDoList) {
        LogisticsChannelDo channelDo = logisticsChannelService.getById(entityDo.getLogisticsChannelId());
        boolean isCustomChannel = Objects.nonNull(channelDo) && Objects.equals(channelDo.getChannelCode(), "custom_channel");
        List<TrialFreightRes> trialFreightResList = new ArrayList<>();
        //自定义渠道不进行运费试算
        if (!isCustomChannel) {
            //运费试算
            trialFreightResList = quotationSchemeService.omsTrialFreightFee(this.buildTrialFreightReq(entityDo));
            if (CollUtil.isEmpty(trialFreightResList)) {
                this.lambdaUpdate()
                        .eq(OrderOutboundDo::getId, entityDo.getId())
                        .set(OrderOutboundDo::getExceptionTime, DateUtil.date())
                        .set(OrderOutboundDo::getStatus, TransferOutboundStatusEnum.five.getValue())
                        .set(OrderOutboundDo::getExceptionType, StringsUtil.createI18Message(SysConstant.Freight_Trial_Failed))
                        .set(OrderOutboundDo::getExceptionReason, StringsUtil.createI18Message(SysConstant.Freight_Trial_Failed))
                        .update();
                return new String[]{SysConstant.Freight_Trial_Failed};
            }
        }
        if (isCustomChannel || trialFreightResList.get(0).isPlaceOrderFlag()) {
            if (Objects.equals(entityDo.getLogisticsChannelId(), -1L)) {
                //自动选取最低价的物流渠道
                TrialFreightRes trialFreightRes = trialFreightResList.get(0);
                LogisticsChannelDo newChannelDo = logisticsChannelService.getById(trialFreightRes.getChannelId());
                this.lambdaUpdate()
                        .eq(OrderOutboundDo::getId, entityDo.getId())
                        .set(OrderOutboundDo::getLogisticsChannelId, newChannelDo.getId())
                        .set(OrderOutboundDo::getCarrier, newChannelDo.getCarrierCode())
                        .update();
            }
            try {
                //锁定库存
                Integer outboundMode = entityDo.getOutboundMode();
                if (outboundMode.equals(2)) {
                    //按箱出库去除相同箱类号的数据
                    outboundProductDoList = outboundProductDoList.stream()
                            .filter(distinctByKey(OrderOutboundProductDo::getBoxTypeNumber))
                            .collect(Collectors.toList());
                }
                for (OrderOutboundProductDo item : outboundProductDoList) {
                    //todo 库位优先级排序??
                    //获取可以锁定的库存数据
                    List<ProductStockRackDo> productStockRackDoList = null;
                    //需要锁定的数量
                    AtomicInteger needLock = new AtomicInteger();
                    if (outboundMode.equals(1)) {
                        productStockRackDoList = productStockRackService.getFirstInFirstOutBySku(
                                tenantId,
                                entityDo.getWhId(),
                                item.getProductId(),
                                item.getForecastQty(),
                                null
                        );
                        needLock.set(item.getForecastQty());
                    } else if (outboundMode.equals(2)) {
                        productStockRackDoList = productStockRackService.getFirstInFirstOutByBoxTypeNumber(
                                tenantId,
                                entityDo.getWhId(),
                                entityDo.getCompanyId(),
                                item.getBoxTypeNumber(),
                                item.getForecastBoxQty(),
                                null
                        );
                        needLock.set(item.getForecastBoxQty());
                    }
                    //锁定库存并保存拣货记录
                    this.lockStockAndSavePicking(
                            productStockRackDoList,
                            needLock,
                            StrUtil.isNotEmpty(item.getBoxTypeNumber()) ? item.getBoxTypeNumber() : String.valueOf(item.getProductId()),
                            item.getId(),
                            Boolean.FALSE
                    );
                }
            } catch (Exception e) {
                // 回退部分库存锁定
                if (CollUtil.isNotEmpty(outboundProductDoList)) {
                    List<OutboundPickingDo> pickingDoList = outboundPickingService.lambdaQuery()
                            .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                            .in(OutboundPickingDo::getOutboundProductId, outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                            .list();
                    if (CollUtil.isNotEmpty(pickingDoList)) {
                        this.unLock(
                                JSONUtil.toList(JSONUtil.toJsonStr(pickingDoList), OutboundPickingDo.class),
                                pickingDoList,
                                new AtomicInteger(pickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum))
                        );
                    }
                }
                //下单失败，不锁定库存，异常处理
                String s = e.getMessage();
                if (s.length() > 100) {
                    s = s.substring(0, 100);
                }
                this.lambdaUpdate()
                        .eq(OrderOutboundDo::getId, entityDo.getId())
                        .set(OrderOutboundDo::getMarkExceptionTime, null)
                        .set(OrderOutboundDo::getExceptionTime, DateUtil.date())
                        .set(OrderOutboundDo::getStatus, TransferOutboundStatusEnum.five.getValue())
                        .set(OrderOutboundDo::getExceptionType, StringsUtil.createI18Message(StaticDict.Order_Outbound_ExceptionType.One.getValue()))
                        .set(OrderOutboundDo::getExceptionReason, s)
                        .update();
                return new String[]{SysConstant.Wms_Stock_Shortage, StringsUtil.createI18Message(e.getMessage())};
            }
        } else {
            //下单失败，不锁定库存，异常处理
            String s = trialFreightResList.get(0).getPlaceOrderDetailMsg();
            if (s.length() > 100) {
                s = s.substring(0, 100);
            }
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, entityDo.getId())
                    .set(OrderOutboundDo::getExceptionTime, DateUtil.date())
                    .set(OrderOutboundDo::getStatus, TransferOutboundStatusEnum.five.getValue())
                    .set(OrderOutboundDo::getExceptionType, StaticDict.Order_Outbound_ExceptionType.Four.getValue())
                    .set(OrderOutboundDo::getExceptionReason, s)
                    .update();
            return new String[]{trialFreightResList.get(0).getPlaceOrderMsg(), trialFreightResList.get(0).getPlaceOrderDetailMsg()};
        }
        return null;
    }

    /**
     * 出库单构建运费试算请求
     */
    @Override
    public TrialOmsReq buildTrialFreightReq(OrderOutboundDo entityDo) {
        TrialOmsReq req = new TrialOmsReq();
//        req.setOrderSellerType();
        req.setInsureFlag(entityDo.getInsuranceFlag());
        req.setInsureMoney((entityDo.getInsureMoney() == null ? 0 : entityDo.getInsureMoney().doubleValue()));
        req.setSignatureType(entityDo.getSignatureService());
        req.setCompanyId(entityDo.getCompanyId());
        req.setProvinceCode(entityDo.getProvinceName());
        req.setCityName(entityDo.getCityName());
        req.setWarehouseId(entityDo.getWhId());
        req.setCountryCode(entityDo.getCountryRegionCode());
        req.setPostcode(entityDo.getPostCode());
        req.setAutoChooseCheap(1);
        req.setLc(Collections.singletonList(new LcCodeRes(entityDo.getLogisticsChannelId(), "", "")));
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, entityDo.getId())
                .list();
        Map<Long, ProductDo> productIdMatchDo = productService.lambdaQuery()
                .in(ProductDo::getId, outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //数据类型 1sku 2箱
        int dataType = entityDo.getOrderType().equals(OutboundTypeEnum.one.getValue())
                ? 1
                : entityDo.getOutboundMode().equals(1) ? 1 : 2;
        switch (dataType) {
            case 1:
                req.setGoodsType(2);
                List<TrialProductReq> products = new ArrayList<>();
                for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                    ProductDo productDo = productIdMatchDo.get(outboundProductDo.getProductId());
                    if (productDo == null) {
                        continue;
                    }
//                    TrialProductReq trialProductReq = new TrialProductReq();
//                    BeanUtils.copyProperties(productDo, trialProductReq);
                    TrialProductReq trialProductReq = BeanConvert.INSTANCE.productDoToTrialProductReq(productDo);
                    trialProductReq.setProductId(productDo.getId());
                    trialProductReq.setSku(productDo.getSku());
                    trialProductReq.setQuantity(outboundProductDo.getForecastQty());
                    trialProductReq.setCategoryId(productDo.getCategoryId());
                    products.add(trialProductReq);
                }
                req.setProducts(products);
                //查询包裹信息
                List<OrderOutboundPackageDo> packageDoList = outboundPackageService.lambdaQuery()
                        .eq(OrderOutboundPackageDo::getOutboundId, entityDo.getId())
                        .list();
                List<TrialBoxReq> boxReqList = new ArrayList<>();
                for (OrderOutboundPackageDo packageDo : packageDoList) {
//                    TrialBoxReq boxReq = new TrialBoxReq();
//                    BeanUtils.copyProperties(packageDo, boxReq);
                    TrialBoxReq boxReq = BeanConvert.INSTANCE.packageDoToTrialBoxReq(packageDo);
                    boxReq.setQuantity(1);
                    if (StrUtil.isNotBlank(entityDo.getTrackingNumber())) {
                        boxReq.setTrackNo(entityDo.getTrackingNumber());
                    }
                    boxReqList.add(boxReq);
                }
                req.setBoxes(boxReqList);
                break;
            case 2:
                req.setGoodsType(3);
                Map<String, OrderBoxDo> boxMatch = boxService.lambdaQuery()
                        .in(OrderBoxDo::getBoxTypeNumber, outboundProductDoList.stream().map(OrderOutboundProductDo::getBoxTypeNumber).collect(Collectors.toSet()))
                        .groupBy(OrderBoxDo::getBoxTypeNumber)
                        .list().stream().collect(Collectors.toMap(OrderBoxDo::getBoxTypeNumber, val -> val, (o1, o2) -> o1));
                outboundProductDoList = outboundProductDoList.stream().filter(distinctByKey(OrderOutboundProductDo::getBoxTypeNumber)).collect(Collectors.toList());
                List<TrialBoxReq> boxes = new ArrayList<>();
                for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                    OrderBoxDo boxDo = boxMatch.get(outboundProductDo.getBoxTypeNumber());
//                    TrialBoxReq trialBoxReq = new TrialBoxReq();
//                    BeanUtils.copyProperties(boxDo, trialBoxReq);
                    TrialBoxReq trialBoxReq = BeanConvert.INSTANCE.boxDoToTrialBoxReq(boxDo);
                    trialBoxReq.setQuantity(outboundProductDo.getForecastBoxQty());
                    trialBoxReq.setTrackNo(outboundProductDo.getBoxTypeNumber());
                    boxes.add(trialBoxReq);
                }
                req.setBoxes(boxes);
                Collection<OrderOutboundProductDo> values = outboundProductDoList.stream().collect(Collectors.toMap(OrderOutboundProductDo::getProductId, val -> val, (o1, o2) -> {
                    o1.setOutboundQty(o1.getOutboundQty() + o2.getOutboundQty());
                    o1.setForecastQty(o1.getForecastQty() + o2.getForecastQty());
                    return o1;
                })).values();
                List<TrialProductReq> products2 = new ArrayList<>();
                for (OrderOutboundProductDo outboundProductDo : values) {
                    ProductDo productDo = productIdMatchDo.get(outboundProductDo.getProductId());
                    if (productDo == null) {
                        continue;
                    }
//                    TrialProductReq trialProductReq = new TrialProductReq();
//                    BeanUtils.copyProperties(productDo, trialProductReq);
                    TrialProductReq trialProductReq = BeanConvert.INSTANCE.productDoToTrialProductReq(productDo);
                    trialProductReq.setProductId(productDo.getId());
                    trialProductReq.setSku(productDo.getSku());
                    trialProductReq.setQuantity(outboundProductDo.getOutboundQty());
                    products2.add(trialProductReq);
                }
                req.setProducts(products2);
                break;
            default:
        }
        return req;
    }

    /**
     * 删除原始记录
     *
     * @param outboundId 出库单id
     */
    public void deleteOriginalDataByOutboundId(Long outboundId) {
        //修改前删除原始记录
        outboundProductService.remove(
                Wrappers.<OrderOutboundProductDo>lambdaQuery()
                        .eq(OrderOutboundProductDo::getOutboundId, outboundId)
        );
        List<OrderOutboundPackageDo> one = outboundPackageService.list(
                Wrappers.<OrderOutboundPackageDo>lambdaQuery()
                        .eq(OrderOutboundPackageDo::getOutboundId, outboundId)
        );
        if (!one.isEmpty()) {
            List<Long> collect = one.stream().map(OrderOutboundPackageDo::getId).collect(Collectors.toList());
            outboundPackageService.removeByIds(collect);
            outboundPackageDetailService.remove(new LambdaQueryWrapper<OrderOutboundPackageDetailDo>()
                    .eq(OrderOutboundPackageDetailDo::getOutboundId, outboundId));
            outboundPackageMaterialService.remove(
                    Wrappers.<OrderOutboundPackageMaterialDo>lambdaQuery()
                            .in(OrderOutboundPackageMaterialDo::getPackageId, collect)
            );
        }
    }

    /**
     * 锁库存并保存拣货记录
     *
     * @param productStockRackDoList   可以锁定的库存数据
     * @param needLock                 需要锁定的数量
     * @param productIdOrBoxTypeNumber cpid或箱类号
     * @param outboundProductId        出库单产品表id
     */
    @Override
    public void lockStockAndSavePicking(
            List<ProductStockRackDo> productStockRackDoList,
            AtomicInteger needLock,
            String productIdOrBoxTypeNumber,
            Long outboundProductId,
            Boolean pickFlag) throws BizException {
        if (CollUtil.isEmpty(productStockRackDoList)) {
            throw new BizException(SysConstant.Inventory_Shortage, productIdOrBoxTypeNumber);
        }
        List<OutboundPickingDo> outboundPickingDoList = new ArrayList<>();
        for (ProductStockRackDo productStockRackDo : productStockRackDoList.stream().sorted(Comparator.comparing(ProductStockRackDo::getId)).collect(Collectors.toList())) {
            if (needLock.get() <= 0) {
                break;
            }
            //该条记录可用数量
            Integer quantity = productStockRackDo.getQuantity();
            if (quantity == 0) {
                continue;
            }
            int qty;
            if (quantity < needLock.get()) {
                qty = quantity;
                needLock.set(needLock.get() - quantity);
            } else {
                qty = needLock.get();
                needLock.set(0);
            }
            //锁定库存
            productStockRackService.lambdaUpdate()
                    .eq(ProductStockRackDo::getId, productStockRackDo.getId())
                    .set(ProductStockRackDo::getQuantity, quantity - qty)
                    .set(ProductStockRackDo::getLockQuantity, productStockRackDo.getLockQuantity() + qty)
                    .update();
            OutboundPickingDo outboundPickingDo = new OutboundPickingDo();
            outboundPickingDo.setOrderType(OutboundOrderKind.outbound.getValue());
            outboundPickingDo.setOutboundProductId(outboundProductId);
            outboundPickingDo.setSubStockId(productStockRackDo.getId());
            outboundPickingDo.setRackId(productStockRackDo.getRackId());
            outboundPickingDo.setQty(qty);
            outboundPickingDo.setPickFlag(pickFlag);
            outboundPickingDoList.add(outboundPickingDo);
        }
        if (needLock.get() > 0) {
            throw new BizException(SysConstant.Inventory_Shortage, productIdOrBoxTypeNumber);
        }
        //批量保存系统默认拣货记录
        outboundPickingService.saveBatch(outboundPickingDoList);
    }

    /**
     * 创建一件代发出库单进度
     */
    @Override
    public void createNormalProgress(OrderOutboundDo entityDo, String username) {
        progressService.batchInsert(this.getProgress(entityDo, username));
    }

    public List<ProgressDo> getProgress(OrderOutboundDo entityDo, String username) {
        List<ProgressDo> progressDoList = new ArrayList<>();
        progressDoList.add(
                ProgressDo.builder()
                        .type(ProgressEnum.OUTBOUND.getTypeCode())
                        .bizNumber(entityDo.getId())
                        .bizType("创建")
                        .operateBy(username)
                        .operateTime(entityDo.getCreateTime() == null ? DateUtil.date() : entityDo.getCreateTime())
                        .sort(1)
                        .build()
        );
        progressDoList.add(
                ProgressDo.builder()
                        .type(ProgressEnum.OUTBOUND.getTypeCode())
                        .bizNumber(entityDo.getId())
                        .bizType("提交")
                        .operateBy(NormalOutboundStatusEnum.one.getValue().equals(entityDo.getStatus()) ? username : null)
                        .operateTime(NormalOutboundStatusEnum.one.getValue().equals(entityDo.getStatus()) ? DateUtil.date() : null)
                        .sort(2)
                        .build()
        );
        if (Objects.equals(entityDo.getSendOut(), 1)) {
            progressDoList.add(
                    ProgressDo.builder()
                            .type(ProgressEnum.OUTBOUND.getTypeCode())
                            .bizNumber(entityDo.getId())
                            .bizType("推送到三方仓")
                            .operateBy(null)
                            .operateTime(null)
                            .sort(3)
                            .build()
            );
        } else {
            progressDoList.add(
                    ProgressDo.builder()
                            .type(ProgressEnum.OUTBOUND.getTypeCode())
                            .bizNumber(entityDo.getId())
                            .bizType("生成波次")
                            .operateBy(null)
                            .operateTime(null)
                            .sort(3)
                            .build()
            );
            progressDoList.add(
                    ProgressDo.builder()
                            .type(ProgressEnum.OUTBOUND.getTypeCode())
                            .bizNumber(entityDo.getId())
                            .bizType("拣货")
                            .operateBy(null)
                            .operateTime(null)
                            .sort(4)
                            .build()
            );
            progressDoList.add(
                    ProgressDo.builder()
                            .type(ProgressEnum.OUTBOUND.getTypeCode())
                            .bizNumber(entityDo.getId())
                            .bizType("复核")
                            .operateBy(null)
                            .operateTime(null)
                            .sort(5)
                            .build()
            );
        }
        progressDoList.add(
                ProgressDo.builder()
                        .type(ProgressEnum.OUTBOUND.getTypeCode())
                        .bizNumber(entityDo.getId())
                        .bizType("出库")
                        .operateBy(null)
                        .operateTime(null)
                        .sort(6)
                        .build()
        );
        return progressDoList;
    }

    /**
     * 创建中转出库单进度
     */
    @Override
    public void createTransferProgress(OrderOutboundDo entityDo, String username) {
        List<ProgressDo> progressDoList = new ArrayList<>();
        progressDoList.add(
                ProgressDo.builder()
                        .type(ProgressEnum.OUTBOUND.getTypeCode())
                        .bizNumber(entityDo.getId())
                        .bizType("创建")
                        .operateBy(username)
                        .operateTime(DateUtil.date())
                        .sort(1)
                        .build()
        );
        progressDoList.add(
                ProgressDo.builder()
                        .type(ProgressEnum.OUTBOUND.getTypeCode())
                        .bizNumber(entityDo.getId())
                        .bizType("提交")
                        .operateBy(NormalOutboundStatusEnum.one.getValue().equals(entityDo.getStatus()) ? username : null)
                        .operateTime(NormalOutboundStatusEnum.one.getValue().equals(entityDo.getStatus()) ? DateUtil.date() : null)
                        .sort(2)
                        .build()
        );
        progressDoList.add(
                ProgressDo.builder()
                        .type(ProgressEnum.OUTBOUND.getTypeCode())
                        .bizNumber(entityDo.getId())
                        .bizType("拣货")
                        .operateBy(null)
                        .operateTime(null)
                        .sort(3)
                        .build()
        );
        progressDoList.add(
                ProgressDo.builder()
                        .type(ProgressEnum.OUTBOUND.getTypeCode())
                        .bizNumber(entityDo.getId())
                        .bizType("出库")
                        .operateBy(null)
                        .operateTime(null)
                        .sort(4)
                        .build()
        );
        progressService.batchInsert(progressDoList);
    }

    /**
     * 提交出库单后置操作
     */
    public void pushOrderOutboundToWmsAfterHandler(OrderOutboundDo outDo, Long tenantId, Long userId, String username) {
        progressService.lambdaUpdate()
                .eq(ProgressDo::getBizNumber, outDo.getId())
                .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                .eq(ProgressDo::getBizType, "提交")
                .set(ProgressDo::getOperateBy, username)
                .set(ProgressDo::getOperateTime, DateUtil.date())
                .update();
        operateRecordService.operateAddRecordAndValue(
                SysConstant.Order_Submit_Wms,
                "",
                outDo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        //提交时间
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, outDo.getId())
                .set(OrderOutboundDo::getStatus, OutboundTypeEnum.one.getValue().equals(outDo.getOrderType())
                        ? NormalOutboundStatusEnum.one.getValue() : TransferOutboundStatusEnum.one.getValue())
                .set(OrderOutboundDo::getPushWmsTime, DateUtil.date())
                .update();

    }

    /**
     * 生成出库单号
     */
    @Override
    public String generateOutboundNo(String orderType) {
        // 每周重置出库单序号
        //生成入库单号
        String orderInputNoSeq = String.valueOf(redisTemplate.opsForValue().increment(RedisKeyConstants.ORDER_OUTBOUND_NO_SEQ));
        //序列号不足六位在前面补0
        if (orderInputNoSeq.length() < 6) {
            orderInputNoSeq = "000000".substring(0, 6 - orderInputNoSeq.length()) + orderInputNoSeq;
        }
        return orderType
               + DateUtil.format(DateUtil.date(), "yyMMdd")
               + orderInputNoSeq;
    }

    /**
     * 生成波次单号
     */
    private String generateWaveNo() {
        // 每周重置波次序号
        //生成波次单号
        String orderInputNoSeq = String.valueOf(redisTemplate.opsForValue().increment(RedisKeyConstants.OUTBOUND_WAVE_NO_SEQ));
        //序列号不足六位在前面补0
        if (orderInputNoSeq.length() < 6) {
            orderInputNoSeq = "00000".substring(0, 6 - orderInputNoSeq.length()) + orderInputNoSeq;
        }
        return "W"
               + DateUtil.format(DateUtil.date(), "yyMMdd")
               + orderInputNoSeq;
    }

    @Override
    public int normalUpdate(NormalOutboundUpdateBo req, Long userId, String username) {
        OrderOutboundDo entityDo = baseMapper.selectById(req.getId());
//        BeanUtils.copyProperties(req, entityDo);
        BeanConvert.INSTANCE.normalOutboundUpdateBoToOutboundDo(req, entityDo);
        operateRecordService.operateAddRecordAndValue(
                SysConstant.OrderOutbound_Normal_Update,
                "",
                req.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        // 调用运费试算获取下单状态
        this.submitNormalOutboundToWmsAfterLockHandler(entityDo, entityDo.getTenantId(), Collections.emptyList());
        for (OutboundPackageDetail packageDetail : req.getOutboundPackageList()) {
            outboundPackageService.lambdaUpdate()
                    .eq(OrderOutboundPackageDo::getId, packageDetail.getId())
                    .set(OrderOutboundPackageDo::getWeight, packageDetail.getWeight())
                    .set(OrderOutboundPackageDo::getWeightBs, packageDetail.getWeight().multiply(BigDecimal.valueOf(2.2046)).setScale(3, RoundingMode.HALF_UP))
                    .set(OrderOutboundPackageDo::getLength, packageDetail.getLength())
                    .set(OrderOutboundPackageDo::getLengthBs, packageDetail.getLength().divide(BigDecimal.valueOf(2.54), 3, RoundingMode.HALF_UP))
                    .set(OrderOutboundPackageDo::getWidth, packageDetail.getWidth())
                    .set(OrderOutboundPackageDo::getWidthBs, packageDetail.getWidth().divide(BigDecimal.valueOf(2.54), 3, RoundingMode.HALF_UP))
                    .set(OrderOutboundPackageDo::getHeight, packageDetail.getHeight())
                    .set(OrderOutboundPackageDo::getHeightBs, packageDetail.getHeight().divide(BigDecimal.valueOf(2.54), 3, RoundingMode.HALF_UP))
                    .set(StrUtil.isNotBlank(packageDetail.getTrackingNumber()), OrderOutboundPackageDo::getTrackingNumber, packageDetail.getTrackingNumber())
                    .set(Objects.nonNull(packageDetail.getLabelFileId()), OrderOutboundPackageDo::getLabelFileId, packageDetail.getLabelFileId())
                    .update();
            outboundPackageMaterialService.remove(
                    Wrappers.<OrderOutboundPackageMaterialDo>lambdaQuery()
                            .eq(OrderOutboundPackageMaterialDo::getPackageId, packageDetail.getId())
            );
            if (CollUtil.isNotEmpty(packageDetail.getPackInfoVoList())) {
                List<OrderOutboundPackageMaterialDo> packageMaterialDoList = JSONUtil.toList(JSONUtil.toJsonStr(packageDetail.getPackInfoVoList()), OrderOutboundPackageMaterialDo.class);
                packageMaterialDoList.forEach(materialDo -> materialDo.setPackageId(packageDetail.getId()));
                outboundPackageMaterialService.saveBatch(packageMaterialDoList);
            }
        }
        String trackingNumberStr = req.getOutboundPackageList().stream()
                .map(OutboundPackageDetail::getTrackingNumber)
                .filter(StrUtil::isNotBlank)
                .distinct()
                .collect(Collectors.joining(","));
        if (StrUtil.isNotBlank(trackingNumberStr)) {
            entityDo.setTrackingNumber(trackingNumberStr);
        }
        if (StrUtil.isEmpty(entityDo.getDeliveryDate())) {
            entityDo.setDeliveryDate("");
        }
        if (StrUtil.isEmpty(entityDo.getDeliveryTimeName())) {
            entityDo.setDeliveryTimeName("");
        }
        return baseMapper.updateById(entityDo);
    }

    @Override
    public void delete(Long id, Long userId) {
        int delete = baseMapper.deleteById(id);
        if (delete > 0) {
            outboundPackageService.remove(Wrappers.<OrderOutboundPackageDo>lambdaQuery().eq(OrderOutboundPackageDo::getOutboundId, id));
            outboundPackageDetailService.remove(Wrappers.<OrderOutboundPackageDetailDo>lambdaQuery().eq(OrderOutboundPackageDetailDo::getOutboundId, id));
            List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                    .eq(OrderOutboundProductDo::getOutboundId, id)
                    .list();
            if (CollUtil.isNotEmpty(outboundProductDoList)) {
                List<OutboundPickingDo> pickingDoList = outboundPickingService.lambdaQuery()
                        .in(OutboundPickingDo::getOutboundProductId, outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                        .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                        .list();
                if (CollUtil.isNotEmpty(pickingDoList)) {
                    this.unLock(
                            JSONUtil.toList(JSONUtil.toJsonStr(pickingDoList), OutboundPickingDo.class),
                            pickingDoList,
                            new AtomicInteger(pickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum))
                    );
                }
                outboundProductService.removeByIds(outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()));
            }
            cancelService.remove(
                    Wrappers.<OutboundCancelDo>lambdaQuery()
                            .eq(OutboundCancelDo::getOrderKind, OutboundOrderKind.outbound.getValue())
                            .eq(OutboundCancelDo::getOutboundId, id)
            );
        }
    }

    @Override
    public NormalOutboundDetail normalOutboundDetail(Long id, Long userId, String source) {
        OrderOutboundDo outboundDo = baseMapper.selectById(id);
//        NormalOutboundDetail normalOutboundDetail = new NormalOutboundDetail();
//        BeanUtils.copyProperties(outboundDo, normalOutboundDetail);
        NormalOutboundDetail normalOutboundDetail = BeanConvert.INSTANCE.outboundDoToNormalOutboundDetail(outboundDo);
        CompanyDo companyDo = companyService.getById(outboundDo.getCompanyId());
        if (companyDo != null) {
            normalOutboundDetail.setCompanyNameCode(companyDo.getName() + "(" + companyDo.getCompanyCode() + ")");
        }
        //物流渠道
        if (outboundDo.getLogisticsChannelId() != null && StrUtil.isBlank(outboundDo.getCarrier())) {
            LogisticsChannelDo logisticsChannelDo = logisticsChannelService.getById(outboundDo.getLogisticsChannelId());
            if (logisticsChannelDo != null) {
                normalOutboundDetail.setCarrier(logisticsChannelDo.getCarrier());
            }
        }
        //查询包裹信息
        normalOutboundDetail.setOutboundPackageList(getPackageListByOutboundId(id));
        //查询出库单产品信息
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, id)
                .list();
        Map<Long, ProductDo> productIdMatchDo = productService.lambdaQuery()
                .in(ProductDo::getId, outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //查产品可用库存
        ProductStockBySkuQueryBo bo = new ProductStockBySkuQueryBo();
        bo.setUserId(userId);
        bo.setSource(source);
        bo.setTenantId(outboundDo.getTenantId());
        bo.setStockAttribute(0);
        bo.setWhIdList(Collections.singletonList(outboundDo.getWhId()));
        bo.setCompanyIdList(Collections.singletonList(outboundDo.getCompanyId()));
        bo.setProductIds(productIdMatchDo.keySet());
        Map<Long, SkuStockPageDto> productIdMatchStock = productStockDao.productListPageBySku(bo).stream().collect(Collectors.toMap(SkuStockPageDto::getProductId, val -> val));
        List<NormalOutboundDetail.ProductInfo> productInfoList = JSONUtil.toList(
                JSONUtil.toJsonStr(outboundProductDoList),
                NormalOutboundDetail.ProductInfo.class
        );
        productInfoList.forEach(productInfo -> {
            ProductDo productDo = productIdMatchDo.get(productInfo.getProductId());
            SkuStockPageDto skuStockDto = productIdMatchStock.get(productInfo.getProductId());
            int availableAmount = skuStockDto == null ? 0 : skuStockDto.getAvailableAmount();
            productInfo.setProductId(productDo.getId());
            productInfo.setSku(productDo.getSku());
            productInfo.setProductName(productDo.getProductName());
            productInfo.setCategoryId(productDo.getCategoryId());
            productInfo.setAvailableAmount(availableAmount);
            BigDecimal declarePrice = productInfo.getDeclarePrice();
            if (Objects.isNull(declarePrice) || declarePrice.compareTo(BigDecimal.ZERO) == 0) {
                productInfo.setDeclarePrice(productDo.getDeclarePrice());
            }
            productInfo.setCurrencyCode(productDo.getCurrencyCode());
            productInfo.setNeedPack((productDo.getNeedPack() != null && productDo.getNeedPack()));
            productInfo.setPackInfo(productDo.getPackInfo());
        });
        normalOutboundDetail.setProductInfoList(productInfoList);
        if (StrUtil.isNotBlank(outboundDo.getAppendixIds())) {
            List<FileVo> fileVoList = fileService.getFileVoByIds(outboundDo.getAppendixIds());
            List<FileRes> fileRes = JSONUtil.toList(JSONUtil.toJsonStr(fileVoList), FileRes.class);
            fileRes.forEach(item -> item.setFileId(item.getId()));
            normalOutboundDetail.setFileVoList(fileRes);
        }
        if (outboundDo.getLabelFileId() != null) {
            normalOutboundDetail.setLabelFile(fileService.getFileMsg(outboundDo.getLabelFileId()));
        }
        if (outboundDo.getSignatureService() != null) {
            normalOutboundDetail.setSignatureService(OutboundSignServiceEnum.getDescByVal(outboundDo.getSignatureService()));
        }
        return normalOutboundDetail;
    }

    @Override
    public TransferOutboundDetail transferOutboundDetail(Long id, Long userId, String source) {
        OrderOutboundDo entityDo = baseMapper.selectById(id);
//        TransferOutboundDetail transferOutboundDetail = new TransferOutboundDetail();
//        BeanUtils.copyProperties(entityDo, transferOutboundDetail);
        TransferOutboundDetail transferOutboundDetail = BeanConvert.INSTANCE.outboundDoToTransferOutboundDetail(entityDo);
        CompanyDo companyDo = companyService.getById(entityDo.getCompanyId());
        if (companyDo != null) {
            transferOutboundDetail.setCompanyNameCode(companyDo.getName() + "(" + companyDo.getCompanyCode() + ")");
        }
        //物流渠道
        if (entityDo.getLogisticsChannelId() != null && StrUtil.isBlank(entityDo.getCarrier())) {
            LogisticsChannelDo logisticsChannelDo = logisticsChannelService.getById(entityDo.getLogisticsChannelId());
            if (logisticsChannelDo != null) {
                transferOutboundDetail.setCarrier(entityDo.getCarrier());
            }
        }
        //查询包裹信息
        transferOutboundDetail.setOutboundPackageList(getPackageListByOutboundId(id));
        //查询出库单产品信息
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, id)
                .list();
        //查产品可用库存
        ProductStockBySkuQueryBo bo = new ProductStockBySkuQueryBo();
        bo.setUserId(userId);
        bo.setSource(source);
        bo.setTenantId(entityDo.getTenantId());
        bo.setStockAttribute(0);
        bo.setWhIdList(Collections.singletonList(entityDo.getWhId()));
        bo.setCompanyIdList(Collections.singletonList(entityDo.getCompanyId()));
        bo.setProductIds(outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toSet()));
        Map<Long, SkuStockPageDto> productIdMatchStock = productStockDao.productListPageBySku(bo)
                .stream().collect(Collectors.toMap(SkuStockPageDto::getProductId, val -> val));
        Map<Long, ProductDo> productIdMatchDo = productService.lambdaQuery()
                .in(ProductDo::getId, outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        Collection<TransferOutboundDetail.TransferProductInfo> productInfoList = JSONUtil.toList(
                JSONUtil.toJsonStr(outboundProductDoList),
                TransferOutboundDetail.TransferProductInfo.class
        );
        List<FileVo> fileVos = fileService.getFileVoByIds(outboundProductDoList.stream()
                .map(OrderOutboundProductDo::getNewBarcodeLabelFile)
                .filter(Objects::nonNull)
                .map(String::valueOf)
                .collect(Collectors.joining(",")));
        Map<Long, FileVo> fileIdMatchVo = fileVos.stream().collect(Collectors.toMap(FileVo::getId, val -> val));
        productInfoList.forEach(productInfo -> {
            ProductDo productDo = productIdMatchDo.get(productInfo.getProductId());
            SkuStockPageDto skuStockDto = productIdMatchStock.get(productInfo.getProductId());
            int availableAmount = skuStockDto == null ? 0 : skuStockDto.getAvailableAmount();
            productInfo.setSku(productDo.getSku());
            productInfo.setFnsku(productDo.getFnSku());
            productInfo.setAvailableAmount(availableAmount);
            productInfo.setProductName(productDo.getProductName());
            productInfo.setCategoryId(productDo.getCategoryId());
            BigDecimal declarePrice = productInfo.getDeclarePrice();
            if (Objects.isNull(declarePrice) || declarePrice.compareTo(BigDecimal.ZERO) == 0) {
                productInfo.setDeclarePrice(productDo.getDeclarePrice());
            }
            productDo.setCurrencyCode(productDo.getCurrencyCode());
            if (Objects.nonNull(productInfo.getNewBarcodeLabelFile())) {
                productInfo.setFileVo(fileIdMatchVo.get(productInfo.getNewBarcodeLabelFile()));
            }
        });
        //合并相同sku
        productInfoList = productInfoList.stream()
                .collect(Collectors.toMap(TransferOutboundDetail.TransferProductInfo::getSku, val -> val, (o1, o2) -> {
                    o1.setForecastQty(o1.getForecastQty() + o2.getForecastQty());
                    o1.setOutboundQty(o1.getOutboundQty() + o2.getOutboundQty());
                    return o1;
                })).values();
        transferOutboundDetail.setProductInfoList(productInfoList);
        Integer outboundMode = transferOutboundDetail.getOutboundMode();
        /*if (outboundMode.equals(1)) {
            //按产品查询出库装箱信息
            List<OutboundBoxDo> outboundBoxDoList = outboundBoxService.lambdaQuery()
                    .eq(OutboundBoxDo::getOutboundId, id)
                    .list();
            if (CollUtil.isNotEmpty(outboundBoxDoList)) {
                Map<Long, List<OutboundBoxProductDo>> boxMatchProductList = outboundBoxProductService.lambdaQuery()
                        .in(OutboundBoxProductDo::getBoxId, outboundBoxDoList.stream().map(OutboundBoxDo::getId).collect(Collectors.toList()))
                        .list().stream().collect(Collectors.groupingBy(OutboundBoxProductDo::getBoxId));
                List<TransferOutboundDetail.PackDetail> packDetails = JSONUtil.toList(JSONUtil.toJsonStr(outboundBoxDoList), TransferOutboundDetail.PackDetail.class);
                for (TransferOutboundDetail.PackDetail packDetail : packDetails) {
                    List<OutboundBoxProductDo> productDoList = boxMatchProductList.get(packDetail.getId());
                    if (CollUtil.isNotEmpty(productDoList)) {
                        List<TransferOutboundDetail.SkuInfo> skuInfoList = JSONUtil.toList(JSONUtil.toJsonStr(productDoList), TransferOutboundDetail.SkuInfo.class);
                        skuInfoList.forEach(skuInfo -> {
                            skuInfo.setProductName(productIdMatchDo.get(skuInfo.getProductId()).getProductName());
                            skuInfo.setSku(productIdMatchDo.get(skuInfo.getProductId()).getSku());
                        });
                        packDetail.setSkuList(skuInfoList);
                    }
                }
                transferOutboundDetail.setPackDetailList(packDetails);
            }
        } else*/
        if (outboundMode.equals(2)) {
            //按箱类号
            Set<String> boxTypeNumberList = outboundProductDoList.stream()
                    .map(OrderOutboundProductDo::getBoxTypeNumber)
                    .collect(Collectors.toSet());
            Map<String, OrderBoxDo> boxTypeNumberMatchDo = boxService.lambdaQuery()
                    .in(OrderBoxDo::getBoxTypeNumber, boxTypeNumberList)
                    .groupBy(OrderBoxDo::getBoxTypeNumber)
                    .list().stream().collect(Collectors.toMap(OrderBoxDo::getBoxTypeNumber, val -> val, (o1, o2) -> o1));
            BoxStockByTypeQueryBo req = new BoxStockByTypeQueryBo();
            req.setUserId(userId);
            req.setSource(source);
            req.setTenantId(entityDo.getTenantId());
            req.setWhIdList(Collections.singletonList(entityDo.getWhId()));
            req.setCompanyIdList(Collections.singletonList(entityDo.getCompanyId()));
            req.setBoxTypeNumbers(boxTypeNumberList);
            req.setStockAttribute(0);
            Map<String, BoxTypeStockPageDto> boxTypeNoMatchStock = productStockDao.boxListPageByType(req)
                    .stream().collect(Collectors.toMap(BoxTypeStockPageDto::getBoxTypeNumber, val -> val));
            for (String boxTypeNumber : boxTypeNumberList) {
                OrderBoxDo boxDo = boxTypeNumberMatchDo.get(boxTypeNumber);
//                TransferOutboundDetail.TransferBoxTypeInfo transferBoxTypeInfo = new TransferOutboundDetail.TransferBoxTypeInfo();
//                BeanUtils.copyProperties(boxDo, transferBoxTypeInfo);
                TransferOutboundDetail.TransferBoxTypeInfo transferBoxTypeInfo = BeanConvert.INSTANCE.boxDoToTransferBoxTypeInfo(boxDo);
                transferBoxTypeInfo.setBoxTypeNumber(boxTypeNumber);
                List<OrderOutboundProductDo> doList = outboundProductDoList.stream()
                        .filter(orderOutboundProductDo -> orderOutboundProductDo.getBoxTypeNumber().equals(boxTypeNumber))
                        .collect(Collectors.toList());
                BoxTypeStockPageDto boxTypeStockDto = boxTypeNoMatchStock.get(boxTypeNumber);
                int availableAmount = boxTypeStockDto == null ? 0 : boxTypeStockDto.getAvailableAmount();
                transferBoxTypeInfo.setAvailableAmount(availableAmount);
                transferBoxTypeInfo.setOutboundBoxQty(doList.get(0).getOutboundBoxQty());
                transferBoxTypeInfo.setForecastBoxQty(doList.get(0).getForecastBoxQty());
                List<TransferOutboundDetail.TransferBoxTypeInfo.TransferBoxTypeInfoSku> boxTypeInfoSkus = JSONUtil.toList(
                        JSONUtil.toJsonStr(doList),
                        TransferOutboundDetail.TransferBoxTypeInfo.TransferBoxTypeInfoSku.class
                );
                boxTypeInfoSkus.forEach(boxTypeInfoSku -> {
                    boxTypeInfoSku.setProductName(productIdMatchDo.get(boxTypeInfoSku.getProductId()).getProductName());
                    boxTypeInfoSku.setSku(productIdMatchDo.get(boxTypeInfoSku.getProductId()).getSku());
                });
                transferBoxTypeInfo.setSkuList(boxTypeInfoSkus);
                transferOutboundDetail.getBoxTypeInfoList().add(transferBoxTypeInfo);
            }
        }
        if (entityDo.getLabelFileId() != null) {
            transferOutboundDetail.setLabelFile(fileService.getFileMsg(entityDo.getLabelFileId()));
        }
        return transferOutboundDetail;
    }

    @Override
    public SingleResult<Map<String, Object>> pageInit(Long userId, Long tenantId, Long companyId, String source) {
        HashMap<String, Object> map = new HashMap<>(16);
        map.put("normalStatusList", NormalOutboundStatusEnum.getList());
        map.put("transferStatusList", TransferOutboundStatusEnum.getList());
        if ("oms".equals(source)) {
            map.put("warehouseList", warehouseService.getWarehouseCompany(companyId, tenantId));
        } else {
            map.put("companyList", companyService.getCompanyList(userId, tenantId, source));
            map.put("warehouseList", warehouseService.getList(tenantId));
        }
        map.put("orderVarietyList", OutboundVarietyTypeEnum.getOrderVarietyTypeList());
        map.put("salesPlatformList", SalesPlatformEnum.getSalesPlatformList());
        map.put("lcCarrierList", logisticsChannelService.lambdaQuery().list());
        map.put("carrierList", logisticsChannelService.lambdaQuery().list().stream().map(LogisticsChannelDo::getCarrier).collect(Collectors.toSet()));
        map.put("productCategoryList", productCategoryService.getCategoryTree(tenantId));
        map.put("destinationCountryList", CountryEnum.getCountryEnumList());
        map.put("outboundOriginList", OutboundOriginEnum.getOrderOriginList());
        if (StaticDict.System_Source.OMS.getValue().equals(source)) {
            map.put("storeList", platformStoreService.getList(companyId));
        } else {
            map.put("storeList", platformStoreService.getList(companyId, tenantId, source));
        }

        map.put("exceptionTypeList", StaticDict.getOrder_Outbound_ExceptionType());
        map.put("companyTagList", addressBookService.listObjs(Wrappers.<AddressBookDo>lambdaQuery().select(AddressBookDo::getCompanyTag).eq(Objects.nonNull(tenantId), AddressBookDo::getTenantId, tenantId).eq(Objects.nonNull(companyId), AddressBookDo::getCompanyId, companyId).isNotNull(AddressBookDo::getCompanyTag), ObjectUtil::toString).stream().filter(StrUtil::isNotBlank).collect(Collectors.toSet()));
        return SingleResult.success(map);
    }

    @Override
    public PageResult<NormalOutboundVo> getNormalListPageApi(NormalOrderOutboundQueryBo bo) {
        if (bo.getStatus() == null || (!NormalOutboundStatusEnum.one.getValue().equals(bo.getStatus()) && !bo.getStatus().equals(NormalOutboundStatusEnum.seven.getValue()))) {
            bo.setStatus(NormalOutboundStatusEnum.one.getValue());
        }
        PageResult<NormalOutboundVo> normalListPage = this.getNormalListPage(bo);
        //获取包裹信息
        List<Long> collect = normalListPage.getData().stream().map(NormalOutboundVo::getId).collect(Collectors.toList());
        if (!collect.isEmpty()) {
            //查询包裹尺寸
            List<OrderOutboundPackageDo> packageDoList = outboundPackageService.lambdaQuery()
                    .in(OrderOutboundPackageDo::getOutboundId, collect)
                    .list();
            //查询包裹明细
            Map<Long, List<OrderOutboundPackageDetailDo>> packageDetailMap = outboundPackageDetailService.lambdaQuery()
                    .in(OrderOutboundPackageDetailDo::getOutboundId, collect)
                    .list().stream().collect(Collectors.groupingBy(OrderOutboundPackageDetailDo::getPackageId));
            if (CollUtil.isNotEmpty(packageDoList)) {
                Map<Long, List<OrderOutboundPackageMaterialDo>> packageMaterialListMatch = outboundPackageMaterialService.lambdaQuery()
                        .in(OrderOutboundPackageMaterialDo::getPackageId, packageDoList.stream().map(OrderOutboundPackageDo::getId).collect(Collectors.toSet()))
                        .list().stream().collect(Collectors.groupingBy(OrderOutboundPackageMaterialDo::getPackageId));
                Map<Long, List<OutboundPackageDetail>> packMap = new HashMap<>();
                for (OrderOutboundPackageDo packageDo : packageDoList) {
//                    OutboundPackageDetail packageDetail = new OutboundPackageDetail();
//                    BeanUtils.copyProperties(packageDo, packageDetail);
                    OutboundPackageDetail packageDetail = BeanConvert.INSTANCE.packageDoToPackageDetail(packageDo);
                    //包裹包材
                    List<OrderOutboundPackageMaterialDo> packageMaterialDoList = packageMaterialListMatch.get(packageDo.getId());
                    if (CollUtil.isNotEmpty(packageMaterialDoList)) {
                        packageDetail.setPackInfoVoList(JSONUtil.toList(JSONUtil.toJsonStr(packageMaterialDoList), PackInfoVo.class));
                    }
                    List<OrderOutboundPackageDetailDo> packDetailList = packageDetailMap.get(packageDo.getId());
                    if (packDetailList != null) {
                        List<OutboundPackProductVo> products = new ArrayList<>();
                        for (OrderOutboundPackageDetailDo orderOutboundPackageDetailDo : packDetailList) {
                            products.add(new OutboundPackProductVo(orderOutboundPackageDetailDo.getSku(), orderOutboundPackageDetailDo.getProductId(), orderOutboundPackageDetailDo.getQty()));
                        }
                        packageDetail.setProducts(products);
                    }
                    packMap.computeIfAbsent(packageDo.getOutboundId(), k -> new ArrayList<>()).add(packageDetail);
                }
                normalListPage.getData().forEach(normalOutboundVo -> {
                    normalOutboundVo.setPackageDetailList(packMap.get(normalOutboundVo.getId()));
                });
            }

        }
        return normalListPage;
    }

    @Override
    public PageResult<NormalOutboundVo> getNormalListPage(NormalOrderOutboundQueryBo bo) {
        Integer current = bo.getCurrent();
        Integer pageSize = bo.getPageSize();
        PageHelper.startPage(current, pageSize);
        List<NormalOutboundVo> list = baseMapper.getNormalListPage(bo);
        PageInfo<NormalOutboundVo> page = new PageInfo<>(list);

//        OmpSystemEntity ompSet = systemSetService.getOmpSet(bo.getTenantId());
        if ("oms".equals(bo.getSource())) {
            //获取计费
            List<Long> collect = page.getList().stream().map(NormalOutboundVo::getId).collect(Collectors.toList());
            Map<Long, List<CostItemForOrderPageDto>> itemMap = costService.getCostItem(collect, StaticDict.Cost_Out_OrderType.First.getValue(), bo.getTenantId())
                    .stream().collect(Collectors.groupingBy(CostItemForOrderPageDto::getOrderId));
            for (NormalOutboundVo et : page.getList()) {
                et.setBillItems(itemMap.get(et.getId()));
            }
        }
        return new PageResult<>(this.handlerNormalListAfter(list, bo), current, pageSize, (int) page.getTotal(), page.getPages());
    }

    /**
     * 查询分页列表之后对列表数据的处理
     */
    private List<NormalOutboundVo> handlerNormalListAfter(List<NormalOutboundVo> list, NormalOrderOutboundQueryBo bo) {
        if (CollUtil.isEmpty(list)) {
            return list;
        }
        if (bo.isApiFlag()) {
//        Map<Long, List<OutboundProductVo>> orderIdMatchProduct = baseMapper.batchGetOutboundSkuInfo(list.stream().map(NormalOutboundVo::getId).collect(Collectors.toList())).stream().collect(Collectors.groupingBy(OutboundProductVo::getOrderId));
            for (NormalOutboundVo vo : list) {
//            vo.setProductDtoList(orderIdMatchProduct.getOrDefault(vo.getId(), new ArrayList<>()));
                if (StrUtil.isNotBlank(vo.getSkuDetail())) {
                    String[] split = vo.getSkuDetail().split(",");
                    List<OutboundProductVo> productVos = new ArrayList<>();
                    for (String s : split) {
                        OutboundProductVo productVo = new OutboundProductVo();
                        productVo.setSku(s.substring(0, s.indexOf("*")));
                        productVo.setQty(Integer.valueOf(s.substring(s.indexOf("*") + 1)));
                        productVo.setOrderId(vo.getId());
                        productVos.add(productVo);
                    }
                    vo.setProductDtoList(productVos);
                }
            }
        }
        return list;
    }

    @Override
    public SingleResult getNormalStatusSta(NormalOrderOutboundQueryBo bo) {
        bo.setStatus(null);
        Map<Integer, Integer> data = baseMapper.getNormalListPageStatusSta(bo).stream().collect(Collectors.toMap(ListPageStatusStaDto::getStatus, ListPageStatusStaDto::getQty));
        List<Map<String, Object>> resultList = new ArrayList<>();
        for (NormalOutboundStatusEnum statusEnum : NormalOutboundStatusEnum.values()) {
            Map<String, Object> map = new HashMap<>(8);
            map.put("id", statusEnum.getValue());
            map.put("name", statusEnum.getDesc());
            map.put("sum", data.getOrDefault(statusEnum.getValue(), 0));
            resultList.add(map);
        }
        return SingleResult.success(resultList);
    }

    @Override
    public PageResult<TransferOutboundApiVo> getTransferListPageApi(TransferOrderOutboundQueryBo bo) {
        if (bo.getStatus() == null || (!Objects.equals(TransferOutboundStatusEnum.one.getValue(), bo.getStatus()) && !Objects.equals(bo.getStatus(), TransferOutboundStatusEnum.six.getValue()))) {
            bo.setStatus(TransferOutboundStatusEnum.one.getValue());
        }
        PageResult<TransferOutboundVo> transferListPage = this.getTransferListPage(bo);
        List<TransferOutboundApiVo> apiVoList = BeanCopierUtils.listCopyMapper(transferListPage.getData(), TransferOutboundVo.class, TransferOutboundApiVo.class);
        return new PageResult<>(
                this.handlerTransferApiListAfter(apiVoList),
                transferListPage.getPageInfo().getCurrent(),
                transferListPage.getPageInfo().getPageSize(),
                transferListPage.getPageInfo().getTotal(),
                transferListPage.getPageInfo().getPageNos()
        );
    }

    private List<TransferOutboundApiVo> handlerTransferApiListAfter(List<TransferOutboundApiVo> list) {
        if (CollUtil.isEmpty(list)) {
            return list;
        }
        Set<Long> whIdSet = list.stream().map(TransferOutboundApiVo::getWhId).collect(Collectors.toSet());
        Map<Long, WarehouseDo> whIdMatchDo = new HashMap<>();
        if (CollUtil.isNotEmpty(whIdSet)) {
            whIdMatchDo = warehouseService.listByIds(whIdSet).stream().collect(Collectors.toMap(WarehouseDo::getId, val -> val));
        }
        Set<Long> channelIdSet = list.stream().map(TransferOutboundApiVo::getLogisticsChannelId).collect(Collectors.toSet());
        Map<Long, LogisticsChannelDo> channelIdMatchDo = new HashMap<>();
        if (CollUtil.isNotEmpty(channelIdSet)) {
            channelIdMatchDo = logisticsChannelService.listByIds(channelIdSet).stream().collect(Collectors.toMap(LogisticsChannelDo::getId, val -> val));
        }
        for (TransferOutboundApiVo vo : list) {
            WarehouseDo warehouseDo = whIdMatchDo.get(vo.getWhId());
            if (warehouseDo != null) {
                vo.setWhName(warehouseDo.getWhNameCn());
            }
            LogisticsChannelDo channelDo = channelIdMatchDo.get(vo.getLogisticsChannelId());
            if (channelDo != null) {
                vo.setLogisticsChannelCode(channelDo.getChannelCode());
            }
        }
        return list;
    }

    @Override
    public PageResult<TransferOutboundVo> getTransferListPage(TransferOrderOutboundQueryBo bo) {
        Integer current = bo.getCurrent();
        Integer pageSize = bo.getPageSize();
        PageHelper.startPage(current, pageSize);
        List<TransferOutboundVo> list = baseMapper.getTransferListPage(bo);
        PageInfo<TransferOutboundVo> page = new PageInfo<>(list);
        if ("oms".equals(bo.getSource())) {
            //获取计费
            List<Long> collect = page.getList().stream().map(TransferOutboundVo::getId).collect(Collectors.toList());
            Map<Long, List<CostItemForOrderPageDto>> itemMap = costService.getCostItem(collect, StaticDict.Cost_Out_OrderType.Second.getValue(), bo.getTenantId())
                    .stream().collect(Collectors.groupingBy(CostItemForOrderPageDto::getOrderId));
            for (TransferOutboundVo et : page.getList()) {
                et.setBillItems(itemMap.get(et.getId()));
            }
        }
        return new PageResult<>(this.handlerTransferListAfter(list, bo), current, pageSize, (int) page.getTotal(), page.getPages());
    }

    /**
     * 查询分页列表之后对列表数据的处理
     */
    private List<TransferOutboundVo> handlerTransferListAfter(List<TransferOutboundVo> list, TransferOrderOutboundQueryBo bo) {
        if (CollUtil.isEmpty(list)) {
            return list;
        }
        if (bo.isApiFlag()) {
//        Map<Long, List<OutboundProductVo>> orderIdMatchProduct = baseMapper.batchGetOutboundSkuInfo(list.stream().map(TransferOutboundVo::getId).collect(Collectors.toList())).stream().collect(Collectors.groupingBy(OutboundProductVo::getOrderId));
            for (TransferOutboundVo vo : list) {
//            vo.setProductDtoList(orderIdMatchProduct.getOrDefault(vo.getId(), new ArrayList<>()));
                if (StrUtil.isNotBlank(vo.getSkuDetail())) {
                    String[] split = vo.getSkuDetail().split(",");
                    List<OutboundProductVo> productVos = new ArrayList<>();
                    for (String s : split) {
                        OutboundProductVo productVo = new OutboundProductVo();
                        productVo.setSku(s.substring(0, s.indexOf("*")));
                        productVo.setQty(Integer.valueOf(s.substring(s.indexOf("*") + 1)));
                        productVo.setOrderId(vo.getId());
                        productVos.add(productVo);
                    }
                    vo.setProductDtoList(productVos);
                }
            }
        }
        return list;
    }

    @Override
    public SingleResult getTransferStatusSta(TransferOrderOutboundQueryBo bo) {
        bo.setStatus(null);
        Map<Integer, Integer> data = baseMapper.getTransferListPageStatusSta(bo).stream().collect(Collectors.toMap(ListPageStatusStaDto::getStatus, ListPageStatusStaDto::getQty));
        List<Map<String, Object>> resultList = new ArrayList<>();
        for (TransferOutboundStatusEnum statusEnum : TransferOutboundStatusEnum.values()) {
            Map<String, Object> map = new HashMap<>(8);
            map.put("id", statusEnum.getValue());
            map.put("name", statusEnum.getDesc());
            map.put("sum", data.getOrDefault(statusEnum.getValue(), 0));
            resultList.add(map);
        }
        return SingleResult.success(resultList);
    }

    @Override
    public SingleResult transferSubmitPick(List<Long> ids, Long userId, String username) {
        if (CollUtil.isEmpty(ids)) {
            throw new BizException(SysConstant.Please_Select_Outbound_Pick);
        }
        for (Long id : ids) {
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.OrderOutbound_Submit_Pick,
                    "",
                    id,
                    userId,
                    username,
                    OperateTypeEnum.OrderOutbound
            );
            progressService.lambdaUpdate()
                    .eq(ProgressDo::getBizNumber, id)
                    .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                    .eq(ProgressDo::getBizType, "提交")
                    .set(ProgressDo::getOperateBy, username)
                    .set(ProgressDo::getOperateTime, DateUtil.date())
                    .update();
            //提交时间
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, id)
//                    .set(OrderOutboundDo::getPushWmsTime, DateUtil.date())
                    .set(OrderOutboundDo::getStatus, TransferOutboundStatusEnum.two.getValue())
                    .update();
        }
        return SingleResult.success();
    }

    @Override
    public TransferOutboundPickingDetail transferPickingDetail(Long id) {
        OrderOutboundDo outboundDo = this.getById(id);
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.Outbound_Order_Does_Not_Exist);
        }
        if (!outboundDo.getStatus().equals(TransferOutboundStatusEnum.two.getValue())) {
            throw new BizException(SysConstant.PickDetail_Status_Error);
        }
//        TransferOutboundPickingDetail detail = new TransferOutboundPickingDetail();
//        BeanUtils.copyProperties(outboundDo, detail);
        TransferOutboundPickingDetail detail = BeanConvert.INSTANCE.outboundDoToTransferOutboundPickingDetail(outboundDo);
        List<OrderOutboundProductDo> outboundProductDoList = new ArrayList<>();
        //出库方式
        Integer outboundMode = outboundDo.getOutboundMode();
        if (outboundMode.equals(1)) {
            outboundProductDoList = outboundProductService.lambdaQuery()
                    .eq(OrderOutboundProductDo::getOutboundId, id)
                    .apply("outbound_qty < forecast_qty")
                    .list();
        } else if (outboundMode.equals(2)) {
            outboundProductDoList = outboundProductService.lambdaQuery()
                    .eq(OrderOutboundProductDo::getOutboundId, id)
                    .apply("outbound_box_qty < forecast_box_qty")
                    .list();
            outboundProductDoList = outboundProductDoList.stream().filter(distinctByKey(OrderOutboundProductDo::getBoxTypeNumber)).collect(Collectors.toList());
//            outboundProductDoList = outboundProductDoList.stream().collect(Collectors.toMap(OrderOutboundProductDo::getBoxTypeNumber, val -> val, (o1, o2) -> {
//                o1.setOutboundBoxQty(o1.getOutboundBoxQty() + o2.getOutboundBoxQty());
//                return o1;
//            })).values().stream().filter(item -> item.getOutboundBoxQty() < item.getForecastBoxQty()).collect(Collectors.toList());
        }
        if (CollUtil.isNotEmpty(outboundProductDoList)) {
            //根据出库产品查询推荐的拣货信息
            Map<Long, List<OutboundPickingDo>> packMatchPicking = outboundPickingService.lambdaQuery()
                    .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                    .in(
                            OutboundPickingDo::getOutboundProductId,
                            outboundProductDoList.stream()
                                    .map(OrderOutboundProductDo::getId)
                                    .collect(Collectors.toList())
                    )
                    .eq(OutboundPickingDo::getPickFlag, Boolean.FALSE)
                    .list().stream()
                    .collect(Collectors.groupingBy(OutboundPickingDo::getOutboundProductId));
            outboundProductDoList = outboundProductDoList.stream()
                    .filter(item -> packMatchPicking.containsKey(item.getId()))
                    .collect(Collectors.toList());
            if (CollUtil.isNotEmpty(outboundProductDoList)) {
                if (outboundMode.equals(1)) {
                    Map<Long, ProductDo> productIdMatchDo = productService.lambdaQuery()
                            .in(
                                    ProductDo::getId,
                                    outboundProductDoList.stream()
                                            .map(OrderOutboundProductDo::getProductId)
                                            .collect(Collectors.toList())
                            )
                            .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
                    List<TransferOutboundPickingDetail.PickingListBySku> pickingListBySkuList = new ArrayList<>();
                    for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                        ProductDo productDo = productIdMatchDo.get(outboundProductDo.getProductId());
                        if (productDo == null) {
                            continue;
                        }
                        TransferOutboundPickingDetail.PickingListBySku pickingListBySku = new TransferOutboundPickingDetail.PickingListBySku();
                        List<OutboundPickingDo> pickingDoList = packMatchPicking.get(outboundProductDo.getId());
                        //合并相同库位
                        pickingDoList = new ArrayList<>(pickingDoList.stream().collect(Collectors.toMap(OutboundPickingDo::getRackId, val -> val, (o1, o2) -> {
                            o1.setQty(o1.getQty() + o2.getQty());
                            return o1;
                        })).values());
                        Map<Long, CellAvailableVo> cellAvailableDtoMap = storageLocationService.getCellAvailableByProductId(
                                CellAvailableBySkuQueryBo.builder()
                                        .tenantId(outboundDo.getTenantId())
                                        .outboundProductId(outboundProductDo.getId())
                                        .productId(outboundProductDo.getProductId())
                                        .cellIdList(
                                                pickingDoList.stream()
                                                        .map(OutboundPickingDo::getRackId)
                                                        .collect(Collectors.toList())
                                        )
                                        .build()
                        ).stream().collect(Collectors.toMap(CellAvailableVo::getCellId, val -> val));
                        for (OutboundPickingDo pickingDo : pickingDoList) {
                            pickingListBySku.getCellInfoList().add(
                                    CellInfo.builder()
                                            .cellId(pickingDo.getRackId())
                                            .cellCode(cellAvailableDtoMap.get(pickingDo.getRackId()).getCellCode())
                                            .availableQty(cellAvailableDtoMap.get(pickingDo.getRackId()).getAvailableQty())
                                            .qty(pickingDo.getQty())
                                            .build()
                            );
                        }
                        pickingListBySku.setOutboundProductId(outboundProductDo.getId());
                        pickingListBySku.setProductId(outboundProductDo.getProductId());
                        pickingListBySku.setSku(productDo.getSku());
                        pickingListBySku.setFnsku(productDo.getFnSku());
                        pickingListBySku.setMainBarcode(productDo.getJanCode());
                        pickingListBySku.setOutboundQty(outboundProductDo.getOutboundQty());
                        pickingListBySku.setForecastQty(outboundProductDo.getForecastQty());
                        pickingListBySkuList.add(pickingListBySku);
                    }
                    detail.setPickingListBySkuList(pickingListBySkuList);
                } else if (outboundMode.equals(2)) {
                    Map<String, OrderBoxDo> boxMatch = boxService.lambdaQuery()
                            .in(OrderBoxDo::getBoxTypeNumber,
                                    outboundProductDoList.stream()
                                            .map(OrderOutboundProductDo::getBoxTypeNumber)
                                            .collect(Collectors.toSet()))
                            .groupBy(OrderBoxDo::getBoxTypeNumber)
                            .list().stream().collect(Collectors.toMap(OrderBoxDo::getBoxTypeNumber, val -> val, (o1, o2) -> o1));
                    List<TransferOutboundPickingDetail.PickingListByBox> pickingListByBoxList = new ArrayList<>();
                    for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                        OrderBoxDo boxDo = boxMatch.get(outboundProductDo.getBoxTypeNumber());
                        if (boxDo == null) {
                            continue;
                        }
                        TransferOutboundPickingDetail.PickingListByBox pickingListByBox = new TransferOutboundPickingDetail.PickingListByBox();
                        List<OutboundPickingDo> pickingDoList = packMatchPicking.get(outboundProductDo.getId());
                        //合并相同库位
                        pickingDoList = new ArrayList<>(pickingDoList.stream().collect(Collectors.toMap(OutboundPickingDo::getRackId, val -> val, (o1, o2) -> {
                            o1.setQty(o1.getQty() + o2.getQty());
                            return o1;
                        })).values());
                        Map<Long, CellAvailableVo> cellAvailableDtoMap = storageLocationService.getCellAvailableByBox(
                                CellAvailableByBoxQueryBo.builder()
                                        .outboundProductId(outboundProductDo.getId())
                                        .boxTypeNumber(outboundProductDo.getBoxTypeNumber())
                                        .cellIdList(
                                                pickingDoList.stream()
                                                        .map(OutboundPickingDo::getRackId)
                                                        .collect(Collectors.toList())
                                        )
                                        .build()
                        ).stream().collect(Collectors.toMap(CellAvailableVo::getCellId, val -> val));
                        for (OutboundPickingDo pickingDo : pickingDoList) {
                            pickingListByBox.getCellInfoList().add(
                                    CellInfo.builder()
                                            .cellId(pickingDo.getRackId())
                                            .cellCode(cellAvailableDtoMap.get(pickingDo.getRackId()).getCellCode())
                                            .availableQty(cellAvailableDtoMap.get(pickingDo.getRackId()).getAvailableQty())
                                            .qty(pickingDo.getQty())
                                            .build()
                            );
                        }
                        pickingListByBox.setOutboundProductId(outboundProductDo.getId());
                        pickingListByBox.setBoxTypeNumber(boxDo.getBoxTypeNumber());
                        pickingListByBox.setCustomizeBarcode(boxDo.getCustomizeBarcode());
                        pickingListByBox.setLength(boxDo.getLength());
                        pickingListByBox.setWidth(boxDo.getWidth());
                        pickingListByBox.setHeight(boxDo.getHeight());
                        pickingListByBox.setWeight(boxDo.getWeight());
                        pickingListByBox.setOutboundBoxQty(outboundProductDo.getOutboundBoxQty());
                        pickingListByBox.setForecastBoxQty(outboundProductDo.getForecastBoxQty());
                        pickingListByBoxList.add(pickingListByBox);
                    }
                    detail.setPickingListByBoxList(pickingListByBoxList);
                }
            }
        }
        return detail;
    }

    @Override
    public Map<String, Object> transferPickingDetailInit(Long userId, Long tenantId, String source) {
        Map<String, Object> map = new HashMap<>(8);
        map.put("companyList", companyService.getCompanyList(userId, tenantId, source));
        map.put("storageAreaList", StorageAreaEnum.getMapList());
        map.put("storageLocationType", StorageLocationServiceImpl.storageLocationTypeMap);
        return map;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean transferPicking(TransferOutboundPickingBo bo, Long userId, String username, Long tenantId) throws BizException {
        OrderOutboundDo outboundDo = this.getById(bo.getId());
        if (!TransferOutboundStatusEnum.two.getValue().equals(outboundDo.getStatus())) {
            throw new BizException(SysConstant.PickDetail_Status_Error);
        }
        operateRecordService.operateAddRecordAndValue(
                SysConstant.Order_Pick,
                "",
                bo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        List<TransferOutboundPickingBo.TransferPickingDetail> pickingDetails = bo.getTransferPickingDetailList();
        if (CollUtil.isEmpty(pickingDetails)) {
            throw new BizException(SysConstant.PickDetail_Empty_Error);
        }
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, bo.getId())
                .list();
        //出库方式
        Integer outboundMode = outboundDo.getOutboundMode();
        for (TransferOutboundPickingBo.TransferPickingDetail pickingDetail : pickingDetails) {
            //查询未拣货的记录（推荐库位）
            List<OutboundPickingDo> noPickingDoList = outboundPickingService.lambdaQuery()
                    .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                    .eq(OutboundPickingDo::getOutboundProductId, pickingDetail.getOutboundProductId())
                    .eq(OutboundPickingDo::getPickFlag, Boolean.FALSE)
                    .list();
            if (CollUtil.isEmpty(noPickingDoList)) {
                continue;
            }
            Map<Long, List<OutboundPickingDo>> noPickingDoListMap = noPickingDoList.stream()
                    .collect(Collectors.groupingBy(OutboundPickingDo::getRackId));
            //实际库位
            List<TransferOutboundPickingBo.TransferPickingCell> pickingCellList = pickingDetail.getPickingCellList();
            Map<Long, TransferOutboundPickingBo.TransferPickingCell> pickingCellMap = pickingCellList.stream()
                    .collect(Collectors.toMap(TransferOutboundPickingBo.TransferPickingCell::getCellId, val -> val, (o1, o2) -> {
                        o1.setQty(o1.getQty() + o2.getQty());
                        return o1;
                    }));
            //计算推荐/实际拣货总数
            Integer noPickingTotalQty = noPickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum);
            Integer pickingTotalQty = pickingCellMap.values().stream().map(TransferOutboundPickingBo.TransferPickingCell::getQty).reduce(0, Integer::sum);
            //比较推荐/实际拣货总数
            if (pickingTotalQty <= noPickingTotalQty) {
                //实际拣货库位在推荐库位中
                for (Map.Entry<Long, List<OutboundPickingDo>> entry : noPickingDoListMap.entrySet()) {
                    Long rackId = entry.getKey();
                    List<OutboundPickingDo> pickingDoList = entry.getValue();
                    //计算推荐库位拣货数量
                    Integer noPickingQty = pickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum);
                    TransferOutboundPickingBo.TransferPickingCell pickingCell = pickingCellMap.get(rackId);
                    if (Objects.isNull(pickingCell)) {
                        continue;
                    }
                    //实际库位拣货数量
                    Integer pickingQty = pickingCell.getQty();
                    if (pickingQty < noPickingQty) {
                        //实际拣货数量小于推荐拣货数量 eg.推荐拣货数量为3,实际拣货数量为1的情况
                        AtomicInteger needUpdate = new AtomicInteger(pickingQty);
                        //修改推荐库位的状态
                        pickingDoList.stream()
                                .sorted(Comparator.comparing(OutboundPickingDo::getId))
                                .forEach(item -> {
                                    if (needUpdate.get() == 0) {
                                        //跳过档次循环
                                        return;
                                    }
                                    if (item.getQty() <= needUpdate.get()) {
                                        outboundPickingService.lambdaUpdate()
                                                .eq(OutboundPickingDo::getId, item.getId())
                                                .set(OutboundPickingDo::getPickFlag, Boolean.TRUE)
                                                .update();
                                        noPickingDoList.forEach(noPickingDo -> {
                                            if (noPickingDo.getId().equals(item.getId())) {
                                                noPickingDo.setPickFlag(Boolean.TRUE);
                                            }
                                        });
                                        needUpdate.set(needUpdate.get() - item.getQty());
                                    } else {
                                        outboundPickingService.lambdaUpdate()
                                                .eq(OutboundPickingDo::getId, item.getId())
                                                .setSql("qty = qty -" + needUpdate.get())
                                                .update();
                                        //修改待拣货列表数据
                                        noPickingDoList.forEach(noPickingDo -> {
                                            if (noPickingDo.getId().equals(item.getId())) {
                                                noPickingDo.setQty(noPickingDo.getQty() - needUpdate.get());
                                            }
                                        });
//                                        OutboundPickingDo pickingDo = new OutboundPickingDo();
//                                        BeanUtils.copyProperties(item, pickingDo);
                                        OutboundPickingDo pickingDo = BeanConvert.INSTANCE.copyPickingDo(item);
                                        pickingDo.setId(null);
                                        pickingDo.setOrderType(OutboundOrderKind.outbound.getValue());
                                        pickingDo.setQty(needUpdate.get());
                                        pickingDo.setPickFlag(Boolean.TRUE);
                                        pickingDo.setCreateTime(DateUtil.date());
                                        outboundPickingService.save(pickingDo);
                                        noPickingDoList.add(pickingDo);
                                        needUpdate.set(0);
                                    }
                                });
                    } else {
                        //实际库位拣货数量等于推荐拣货数量的部分
                        outboundPickingService.lambdaUpdate()
                                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                                .eq(OutboundPickingDo::getOutboundProductId, pickingDetail.getOutboundProductId())
                                .eq(OutboundPickingDo::getRackId, rackId)
                                .set(OutboundPickingDo::getPickFlag, Boolean.TRUE)
                                .update();
                        noPickingDoList.forEach(noPickingDo -> {
                            if (noPickingDo.getOutboundProductId().equals(pickingDetail.getOutboundProductId())) {
                                if (noPickingDo.getRackId().equals(rackId)) {
                                    noPickingDo.setPickFlag(Boolean.TRUE);
                                }
                            }
                        });
                        if (pickingQty > noPickingQty) {
                            //实际库位拣货数量大于推荐拣货数量的部分
                            //锁定库存
                            AtomicInteger needLock = new AtomicInteger(pickingQty - noPickingQty);
                            if (outboundMode.equals(1)) {
                                List<ProductStockRackDo> productStockRackDoList = productStockRackService.getFirstInFirstOutBySku(
                                        tenantId,
                                        outboundDo.getWhId(),
                                        Long.valueOf(pickingDetail.getProductIdOrBoxTypeNumber()),
                                        needLock.get(),
                                        pickingCell.getCellId()
                                );
                                this.lockStockAndSavePicking(
                                        productStockRackDoList,
                                        needLock,
                                        pickingDetail.getProductIdOrBoxTypeNumber(),
                                        pickingDetail.getOutboundProductId(),
                                        Boolean.TRUE
                                );
                            } else if (outboundMode.equals(2)) {
                                List<ProductStockRackDo> productStockRackDoList = productStockRackService.getFirstInFirstOutByBoxTypeNumber(
                                        tenantId,
                                        outboundDo.getWhId(),
                                        outboundDo.getCompanyId(),
                                        pickingDetail.getProductIdOrBoxTypeNumber(),
                                        needLock.get(),
                                        pickingCell.getCellId()
                                );
                                //锁定库存
                                this.lockStockAndSavePicking(
                                        productStockRackDoList,
                                        needLock,
                                        pickingDetail.getProductIdOrBoxTypeNumber(),
                                        pickingDetail.getOutboundProductId(),
                                        Boolean.TRUE
                                );
                            }
                            //解锁库存,优先解锁非选择的库位，再根据推荐库位解锁倒序解锁
                            AtomicInteger needUnlock = new AtomicInteger(pickingQty - noPickingQty);
                            List<OutboundPickingDo> unLckPickingList1 = noPickingDoList.stream()
                                    .filter(noPickingDo -> !pickingCellMap.containsKey(noPickingDo.getRackId())
                                                           && Boolean.FALSE.equals(noPickingDo.getPickFlag()))
                                    .collect(Collectors.toList());
                            this.unLock(unLckPickingList1, noPickingDoList, needUnlock);
                            List<OutboundPickingDo> unLockPickingList2 = noPickingDoList.stream()
                                    .filter(noPickingDo -> pickingCellMap.containsKey(noPickingDo.getRackId())
                                                           && Boolean.FALSE.equals(noPickingDo.getPickFlag()))
                                    .collect(Collectors.toList());
                            this.unLock(unLockPickingList2, noPickingDoList, needUnlock);
                        }
                    }
                }
                //实际库位不在推荐库位范围内
                for (Map.Entry<Long, TransferOutboundPickingBo.TransferPickingCell> pickingCellEntry : pickingCellMap.entrySet()) {
                    Long cellId = pickingCellEntry.getKey();
                    Integer qty = pickingCellEntry.getValue().getQty();
                    if (noPickingDoListMap.containsKey(cellId)) {
                        continue;
                    }
                    //锁定库存
                    AtomicInteger needLock = new AtomicInteger(qty);
                    if (outboundMode.equals(1)) {
                        List<ProductStockRackDo> productStockRackDoList = productStockRackService.getFirstInFirstOutBySku(
                                tenantId,
                                outboundDo.getWhId(),
                                Long.valueOf(pickingDetail.getProductIdOrBoxTypeNumber()),
                                needLock.get(),
                                cellId
                        );
                        this.lockStockAndSavePicking(
                                productStockRackDoList,
                                needLock,
                                pickingDetail.getProductIdOrBoxTypeNumber(),
                                pickingDetail.getOutboundProductId(),
                                Boolean.TRUE
                        );
                    } else if (outboundMode.equals(2)) {
                        List<ProductStockRackDo> productStockRackDoList = productStockRackService.getFirstInFirstOutByBoxTypeNumber(
                                tenantId,
                                outboundDo.getWhId(),
                                outboundDo.getCompanyId(),
                                pickingDetail.getProductIdOrBoxTypeNumber(),
                                needLock.get(),
                                cellId
                        );
                        //锁定库存
                        this.lockStockAndSavePicking(
                                productStockRackDoList,
                                needLock,
                                pickingDetail.getProductIdOrBoxTypeNumber(),
                                pickingDetail.getOutboundProductId(),
                                Boolean.TRUE
                        );
                    }
                    //解锁库存,优先解锁非选择的库位，再根据推荐库位解锁倒序解锁
                    AtomicInteger needUnlock = new AtomicInteger(qty);
                    List<OutboundPickingDo> unLckPickingList1 = noPickingDoList.stream()
                            .filter(noPickingDo -> !pickingCellMap.containsKey(noPickingDo.getRackId())
                                                   && Boolean.FALSE.equals(noPickingDo.getPickFlag()))
                            .collect(Collectors.toList());
                    this.unLock(unLckPickingList1, noPickingDoList, needUnlock);
                    List<OutboundPickingDo> unLockPickingList2 = noPickingDoList.stream()
                            .filter(noPickingDo -> pickingCellMap.containsKey(noPickingDo.getRackId())
                                                   && Boolean.FALSE.equals(noPickingDo.getPickFlag()))
                            .collect(Collectors.toList());
                    this.unLock(unLockPickingList2, noPickingDoList, needUnlock);
                }
            } else {
                //超拣
                throw new BizException(SysConstant.PickingQty_Exceeds_ShouldPickingQty, pickingDetail.getProductIdOrBoxTypeNumber());
            }
            //修改出库单产品实际拣货数量
            outboundProductService.lambdaUpdate()
                    .eq(OrderOutboundProductDo::getOutboundId, bo.getId())
                    .eq(outboundMode.equals(1), OrderOutboundProductDo::getProductId, pickingDetail.getProductIdOrBoxTypeNumber())
                    .eq(outboundMode.equals(2), OrderOutboundProductDo::getBoxTypeNumber, pickingDetail.getProductIdOrBoxTypeNumber())
                    .setSql(outboundMode.equals(1), "outbound_qty = outbound_qty + " + pickingTotalQty)
                    .setSql(
                            outboundMode.equals(2),
                            "outbound_qty = outbound_qty + forecast_qty / forecast_box_qty * " + pickingTotalQty + ",\n" +
                            "outbound_box_qty = outbound_box_qty + " + pickingTotalQty
                    )
                    .update();
        }
        boolean pickingFinish = false;
        List<TransferOutboundPickingBo.TransferPickingCell> pickingCellList = pickingDetails.stream()
                .map(TransferOutboundPickingBo.TransferPickingDetail::getPickingCellList)
                .flatMap(List::stream)
                .collect(Collectors.toList());
        //计算本次拣货件数
        Integer thisTimePickingQty = pickingCellList.stream()
                .map(TransferOutboundPickingBo.TransferPickingCell::getQty)
                .reduce(0, Integer::sum);
        //计算应拣数
        if (outboundMode.equals(1)) {
            Integer shouldPickingQty = outboundProductDoList.stream()
                    .map(item -> item.getOutboundQty() < item.getForecastQty() ? item.getForecastQty() - item.getOutboundQty() : 0)
                    .reduce(0, Integer::sum);
            pickingFinish = thisTimePickingQty.equals(shouldPickingQty);
        } else if (outboundMode.equals(2)) {
            Integer shouldPickingQty = outboundProductDoList.stream()
                    .filter(distinctByKey(OrderOutboundProductDo::getBoxTypeNumber))
                    .map(item -> item.getOutboundBoxQty() < item.getForecastBoxQty() ? item.getForecastBoxQty() - item.getOutboundBoxQty() : 0)
                    .reduce(0, Integer::sum);
            pickingFinish = thisTimePickingQty.equals(shouldPickingQty);
        }
        if (pickingFinish) {
            //拣货完成
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.OrderOutbound_Pick_Finish,
                    "",
                    bo.getId(),
                    userId,
                    username,
                    OperateTypeEnum.OrderOutbound
            );
            //进度
            progressService.lambdaUpdate()
                    .eq(ProgressDo::getBizNumber, bo.getId())
                    .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                    .eq(ProgressDo::getBizType, "拣货")
                    .set(ProgressDo::getOperateBy, username)
                    .set(ProgressDo::getOperateTime, DateUtil.date())
                    .update();
            //拣货时间&状态
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, bo.getId())
                    .set(OrderOutboundDo::getPickingTime, DateUtil.date())
                    .set(OrderOutboundDo::getStatus, TransferOutboundStatusEnum.three.getValue())
                    .update();
            return true;
        }
        return false;
    }

    @Override
    public List<TransferPickingByBatchDetail> transferPickingDetailBatch(List<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            throw new BizException(SysConstant.Outbound_Order_Does_Not_Exist);
        }
        if (ids.size() > 20) {
            throw new BizException(SysConstant.Batch_Over_20);
        }
        List<TransferPickingByBatchDetail> detailList = new ArrayList<>();
        Collection<OrderOutboundDo> outboundDoList = this.listByIds(ids);
        for (OrderOutboundDo outboundDo : outboundDoList) {
            Integer outboundMode = outboundDo.getOutboundMode();
            TransferPickingByBatchDetail detail = new TransferPickingByBatchDetail();
            detail.setId(outboundDo.getId());
            detail.setOutboundNo(outboundDo.getOutboundNo());
            detail.setCompanyId(outboundDo.getCompanyId());
            detail.setOutboundMode(outboundMode);
            detail.setTrackingNumber(outboundDo.getTrackingNumber());
            detail.setRemark(outboundDo.getRemark());
            List<OrderOutboundProductDo> outboundProductDoList = new ArrayList<>();
            if (outboundMode.equals(1)) {
                outboundProductDoList = outboundProductService.lambdaQuery()
                        .eq(OrderOutboundProductDo::getOutboundId, outboundDo.getId())
                        .apply("outbound_qty < forecast_qty")
                        .list();
            } else if (outboundMode.equals(2)) {
                outboundProductDoList = outboundProductService.lambdaQuery()
                        .eq(OrderOutboundProductDo::getOutboundId, outboundDo.getId())
                        .apply("outbound_box_qty < forecast_box_qty")
                        .list();
                outboundProductDoList = outboundProductDoList.stream().filter(distinctByKey(OrderOutboundProductDo::getBoxTypeNumber)).collect(Collectors.toList());
            }
            if (CollUtil.isNotEmpty(outboundProductDoList)) {
                //根据出库产品查询推荐的拣货信息
                Map<Long, List<OutboundPickingDo>> packMatchPicking = outboundPickingService.lambdaQuery()
                        .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                        .in(
                                OutboundPickingDo::getOutboundProductId,
                                outboundProductDoList.stream()
                                        .map(OrderOutboundProductDo::getId)
                                        .collect(Collectors.toList())
                        )
                        .eq(OutboundPickingDo::getPickFlag, Boolean.FALSE)
                        .list().stream()
                        .collect(Collectors.groupingBy(OutboundPickingDo::getOutboundProductId));
                outboundProductDoList = outboundProductDoList.stream()
                        .filter(item -> packMatchPicking.containsKey(item.getId()))
                        .collect(Collectors.toList());
                if (CollUtil.isNotEmpty(outboundProductDoList)) {
                    if (outboundMode.equals(1)) {
                        Map<Long, ProductDo> productIdMatchDo = productService.lambdaQuery()
                                .in(
                                        ProductDo::getId,
                                        outboundProductDoList.stream()
                                                .map(OrderOutboundProductDo::getProductId)
                                                .collect(Collectors.toList())
                                )
                                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
                        List<TransferPickingByBatchDetail.PickingListByBatch> pickingListByBatchList = new ArrayList<>();
                        for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                            ProductDo productDo = productIdMatchDo.get(outboundProductDo.getProductId());
                            if (productDo == null) {
                                continue;
                            }
                            TransferPickingByBatchDetail.PickingListByBatch pickingListByBatch = new TransferPickingByBatchDetail.PickingListByBatch();
                            List<OutboundPickingDo> pickingDoList = packMatchPicking.get(outboundProductDo.getId());
                            //合并相同库位
                            pickingDoList = new ArrayList<>(pickingDoList.stream().collect(Collectors.toMap(OutboundPickingDo::getRackId, val -> val, (o1, o2) -> {
                                o1.setQty(o1.getQty() + o2.getQty());
                                return o1;
                            })).values());
                            Map<Long, CellAvailableVo> cellAvailableDtoMap = storageLocationService.getCellAvailableByProductId(
                                    CellAvailableBySkuQueryBo.builder()
                                            .tenantId(outboundDo.getTenantId())
                                            .outboundProductId(outboundProductDo.getId())
                                            .productId(outboundProductDo.getProductId())
                                            .cellIdList(
                                                    pickingDoList.stream()
                                                            .map(OutboundPickingDo::getRackId)
                                                            .collect(Collectors.toList())
                                            )
                                            .build()
                            ).stream().collect(Collectors.toMap(CellAvailableVo::getCellId, val -> val));
                            for (OutboundPickingDo pickingDo : pickingDoList) {
                                pickingListByBatch.getCellInfoList().add(
                                        CellInfo.builder()
                                                .cellId(pickingDo.getRackId())
                                                .cellCode(cellAvailableDtoMap.get(pickingDo.getRackId()).getCellCode())
                                                .availableQty(cellAvailableDtoMap.get(pickingDo.getRackId()).getAvailableQty())
                                                .qty(pickingDo.getQty())
                                                .build()
                                );
                            }
                            pickingListByBatch.setOutboundProductId(outboundProductDo.getId());
                            pickingListByBatch.setProductIdOrBoxTypeNumber(String.valueOf(outboundProductDo.getProductId()));
                            pickingListByBatch.setSku(productDo.getSku());
                            pickingListByBatch.setJanCodeOrItf(productDo.getJanCode());
                            pickingListByBatch.setOutboundQty(outboundProductDo.getOutboundQty());
                            pickingListByBatch.setForecastQty(outboundProductDo.getForecastQty());
                            pickingListByBatchList.add(pickingListByBatch);
                        }
                        detail.setPickingByBatchList(pickingListByBatchList);
                    } else if (outboundMode.equals(2)) {
                        Map<String, OrderBoxDo> boxMatch = boxService.lambdaQuery()
                                .in(OrderBoxDo::getBoxTypeNumber, outboundProductDoList.stream().map(OrderOutboundProductDo::getBoxTypeNumber).collect(Collectors.toSet()))
                                .groupBy(OrderBoxDo::getBoxTypeNumber)
                                .list().stream().collect(Collectors.toMap(OrderBoxDo::getBoxTypeNumber, val -> val, (o1, o2) -> o1));
                        List<TransferPickingByBatchDetail.PickingListByBatch> pickingListByBatchList = new ArrayList<>();
                        for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                            OrderBoxDo boxDo = boxMatch.get(outboundProductDo.getBoxTypeNumber());
                            if (boxDo == null) {
                                continue;
                            }
                            TransferPickingByBatchDetail.PickingListByBatch pickingListByBatch = new TransferPickingByBatchDetail.PickingListByBatch();
                            List<OutboundPickingDo> pickingDoList = packMatchPicking.get(outboundProductDo.getId());
                            //合并相同库位
                            pickingDoList = new ArrayList<>(pickingDoList.stream().collect(Collectors.toMap(OutboundPickingDo::getRackId, val -> val, (o1, o2) -> {
                                o1.setQty(o1.getQty() + o2.getQty());
                                return o1;
                            })).values());
                            Map<Long, CellAvailableVo> cellAvailableDtoMap = storageLocationService.getCellAvailableByBox(
                                    CellAvailableByBoxQueryBo.builder()
                                            .outboundProductId(outboundProductDo.getId())
                                            .boxTypeNumber(outboundProductDo.getBoxTypeNumber())
                                            .cellIdList(
                                                    pickingDoList.stream()
                                                            .map(OutboundPickingDo::getRackId)
                                                            .collect(Collectors.toList())
                                            )
                                            .build()
                            ).stream().collect(Collectors.toMap(CellAvailableVo::getCellId, val -> val));
                            for (OutboundPickingDo pickingDo : pickingDoList) {
                                pickingListByBatch.getCellInfoList().add(
                                        CellInfo.builder()
                                                .cellId(pickingDo.getRackId())
                                                .cellCode(cellAvailableDtoMap.get(pickingDo.getRackId()).getCellCode())
                                                .availableQty(cellAvailableDtoMap.get(pickingDo.getRackId()).getAvailableQty())
                                                .qty(pickingDo.getQty())
                                                .build()
                                );
                            }
                            pickingListByBatch.setOutboundProductId(outboundProductDo.getId());
                            pickingListByBatch.setProductIdOrBoxTypeNumber(boxDo.getBoxTypeNumber());
                            pickingListByBatch.setJanCodeOrItf(boxDo.getCustomizeBarcode());
                            pickingListByBatch.setOutboundQty(outboundProductDo.getOutboundBoxQty());
                            pickingListByBatch.setForecastQty(outboundProductDo.getForecastBoxQty());
                            pickingListByBatchList.add(pickingListByBatch);
                        }
                        detail.setPickingByBatchList(pickingListByBatchList);
                    }
                }
            }
            detailList.add(detail);
        }
        return detailList;
    }

    @Override
    public List<Long> transferPickingBatch(TransferPickingBatchBo bo, Long userId, String username, Long tenantId) {
        List<Long> successIds = new ArrayList<>();
        for (TransferOutboundPickingBo transferOutboundPickingBo : bo.getBatchPickingBoList()) {
            boolean pickingFlag = transferPicking(transferOutboundPickingBo, userId, username, tenantId);
            if (pickingFlag) {
                successIds.add(transferOutboundPickingBo.getId());
            }
        }
        return successIds;
    }

    @Override
    public void unLock(List<OutboundPickingDo> unLockPickingList, List<OutboundPickingDo> noPickingDoList, AtomicInteger needUnlock) {
        unLockPickingList.stream()
                .sorted(Comparator.comparing(OutboundPickingDo::getSubStockId))
                .forEach(item -> {
                    if (needUnlock.get() == 0) {
                        //跳过档次循环
                        return;
                    }
                    if (item.getQty() <= needUnlock.get()) {
                        productStockRackService.lambdaUpdate()
                                .eq(ProductStockRackDo::getId, item.getSubStockId())
                                .setSql("quantity = quantity + " + item.getQty())
                                .setSql("lock_quantity = lock_quantity - " + item.getQty())
                                .update();
                        outboundPickingService.removeById(item.getId());
                        noPickingDoList.forEach(noPickingDo -> {
                            if (noPickingDo.getId().equals(item.getId())) {
                                noPickingDo.setPickFlag(null);
                            }
                        });
                        needUnlock.set(needUnlock.get() - item.getQty());
                    } else {
                        productStockRackService.lambdaUpdate()
                                .eq(ProductStockRackDo::getId, item.getSubStockId())
                                .setSql("quantity = quantity + " + needUnlock.get())
                                .setSql("lock_quantity = lock_quantity - " + item.getQty())
                                .update();
                        outboundPickingService.lambdaUpdate()
                                .eq(OutboundPickingDo::getId, item.getId())
                                .setSql("qty = qty -" + needUnlock.get())
                                .update();
                        //修改待拣货列表数据
                        noPickingDoList.forEach(noPickingDo -> {
                            if (noPickingDo.getId().equals(item.getId())) {
                                noPickingDo.setQty(noPickingDo.getQty() - needUnlock.get());
                            }
                        });
                        needUnlock.set(0);
                    }
                });
    }

    @Override
    public boolean deductionOfInventory(DeductionOfInventoryBo bo) {
        if (CollUtil.isEmpty(bo.getSkuInfoList())) {
            return false;
        }
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, bo.getSkuInfoList().stream().map(DeductionOfInventoryBo.SkuInfo::getProductId).collect(Collectors.toSet()))
                .list()
                .stream()
                .collect(Collectors.toMap(ProductDo::getId, val -> val));
        Map<String, List<ProductStockRackDo>> productIdMatchStockRack = new HashMap<>();
        for (DeductionOfInventoryBo.SkuInfo skuInfo : bo.getSkuInfoList()) {
            List<ProductStockRackDo> productStockRackDoList = productStockRackService.getFirstInFirstOutBySku(
                    bo.getTenantId(),
                    bo.getWhId(),
                    skuInfo.getProductId(),
                    skuInfo.getQty(),
                    null
            );
            if (CollUtil.isEmpty(productStockRackDoList)) {
//                throw new BizException(SysConstant.Inventory_Shortage, skuInfo.getProductId());
                return false;
            }
            int quantityQty = productStockRackDoList.stream()
                    .map(ProductStockRackDo::getQuantity)
                    .reduce(0, Integer::sum);
            if (quantityQty < skuInfo.getQty()) {
//                throw new BizException(SysConstant.Inventory_Shortage, skuInfo.getProductId());
                return false;
            }
            productIdMatchStockRack.put(skuInfo.getProductId() + ":" + skuInfo.getQty(), productStockRackDoList);
        }
        //查询库位信息 用于匹配库存属性
        StorageLocationQueryBo slBo = new StorageLocationQueryBo();
        slBo.setIdList(productIdMatchStockRack.values().stream().flatMap(List::stream).map(ProductStockRackDo::getRackId).collect(Collectors.toSet()));
        slBo.setTenantId(bo.getTenantId());
        slBo.setPageSize(1000);
        Map<Long, StorageLocationVo> cellIdMatchDo = storageLocationService.getListPage(slBo).getData()
                .stream().collect(Collectors.toMap(StorageLocationVo::getId, val -> val));
        String batchNo = DateUtils.dateFormatToString(DateUtil.date(), "yyyyMMdd");
        for (Map.Entry<String, List<ProductStockRackDo>> entry : productIdMatchStockRack.entrySet()) {
            Long productId = Long.parseLong(entry.getKey().split(":")[0]);
            // 需要扣减的数量
            AtomicInteger needDeductionQty = new AtomicInteger(Integer.parseInt(entry.getKey().split(":")[1]));
            for (ProductStockRackDo productStockRackDo : entry.getValue().stream().sorted(Comparator.comparing(ProductStockRackDo::getId)).collect(Collectors.toList())) {
                if (needDeductionQty.get() <= 0) {
                    break;
                }
                //该条记录可用数量
                Integer quantity = productStockRackDo.getQuantity();
                if (quantity == 0) {
                    continue;
                }
                int qty;
                if (quantity < needDeductionQty.get()) {
                    qty = quantity;
                    needDeductionQty.set(needDeductionQty.get() - quantity);
                } else {
                    qty = needDeductionQty.get();
                    needDeductionQty.set(0);
                }
                //扣减库存
                productStockRackService.lambdaUpdate()
                        .eq(ProductStockRackDo::getId, productStockRackDo.getId())
                        .set(ProductStockRackDo::getQuantity, quantity - qty)
                        .update();
                //库存流水
                stockFlowingService.insertProForOut(
                        ProductFlowingOutEt.builder()
                                .tenantId(bo.getTenantId())
                                .companyId(bo.getCompanyId())
                                .warehouseId(bo.getWhId())
                                .sku(productIdMatch.get(productId).getSku())
                                .rackId(productStockRackDo.getRackId())
                                .stockAttribute(cellIdMatchDo.get(productStockRackDo.getRackId()).getMainType())
                                .bookQuantity(-qty)
                                .orderType(bo.getOrderType().getValue())
                                .noteNo(bo.getOrderNo())
                                .innerBatchNo(batchNo)
                                .type(StockTypeEnum.one.getValue())
                                .build()
                );
            }
        }
        return true;
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public CommonBatchOperationVo batchUpdateWhAndChannel(OutboundBatchUpdateWhAndChannelBo bo, Long userId, String username, Long companyId) {
        CommonBatchOperationVo vo = new CommonBatchOperationVo();
        if (CollUtil.isEmpty(bo.getOutboundIdList())) {
            return vo;
        }
        WarehouseDo warehouseDo = warehouseService.getById(bo.getWhId());
        Integer warehouseType = warehouseDo.getWarehouseType();
        LogisticsChannelDo logisticsChannelDo = null;
        if (Objects.nonNull(bo.getLogisticsChannelId())) {
            logisticsChannelDo = logisticsChannelService.getById(bo.getLogisticsChannelId());
        }
        if (bo.getOutboundIdList().size() > 1000) {
            throw new BizException(SysConstant.Batch_Over_1000);
        }
        Collection<OrderOutboundDo> outboundDoList = this.listByIds(bo.getOutboundIdList());
        vo.setTotal(outboundDoList.size());
        List<Long> needUpdateStockOutIdList = outboundDoList.stream().filter(item -> NormalOutboundStatusEnum.one.getValue().equals(item.getStatus()) && !item.getWhId().equals(warehouseDo.getId())).map(OrderOutboundDo::getId).collect(Collectors.toList());
        Map<Long, List<OrderOutboundProductDo>> outIdMatchProductList = new HashMap<>();
        if (CollUtil.isNotEmpty(needUpdateStockOutIdList)) {
            outIdMatchProductList = outboundProductService.lambdaQuery().in(OrderOutboundProductDo::getOutboundId, needUpdateStockOutIdList).list().stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getOutboundId));
        }
        List<ProgressDo> progressDoList = new ArrayList<>();
        List<Long> outIdList = new ArrayList<>();
        String key = RedisKeyConstants.OUTBOUND_BATCH_UPDATE_WH_AND_CHANNEL;
        for (OrderOutboundDo outboundDo : outboundDoList) {
            if (outboundDo.getStatus() > NormalOutboundStatusEnum.one.getValue()) {
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", outboundDo.getOutboundNo());
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Warehouse_Processing_Modify_Fail));
                vo.getFailedList().add(map);
                vo.setFailed(vo.getFailed() + 1);
                continue;
            }
            RLock lock = redissonClient.getLock(key + outboundDo.getId());
            Map<Long, List<OrderOutboundProductDo>> finalOutIdMatchProductList = outIdMatchProductList;
            LogisticsChannelDo finalLogisticsChannelDo = logisticsChannelDo;
            boolean isLocked = false;
            try {
                //最多等锁3秒，最多持有锁30秒
                isLocked = lock.tryLock(3, 30, TimeUnit.SECONDS);
                if (isLocked) {
                    // 业务逻辑
                    Boolean executeStatus = transactionTemplate.execute(status -> {
                        try {
                            //判断是否需要锁定/释放库存
                            if (NormalOutboundStatusEnum.one.getValue().equals(outboundDo.getStatus()) && !outboundDo.getWhId().equals(warehouseDo.getId())) {
                                //释放库存并根据仓库重新锁定
                                List<OrderOutboundProductDo> outboundProductDoList = finalOutIdMatchProductList.get(outboundDo.getId());
                                if (CollUtil.isNotEmpty(outboundProductDoList)) {
                                    Map<Long, List<ProductStockRackDo>> outProductIdMatchStockRack = new HashMap<>();
                                    //判断目标仓库是否有库存
                                    for (OrderOutboundProductDo item : outboundProductDoList) {
                                        List<ProductStockRackDo> productStockRackDoList = productStockRackService.getFirstInFirstOutBySku(
                                                outboundDo.getTenantId(),
                                                warehouseDo.getId(),
                                                item.getProductId(),
                                                item.getForecastQty(),
                                                null
                                        );
                                        if (CollUtil.isEmpty(productStockRackDoList) || productStockRackDoList.stream().map(ProductStockRackDo::getQuantity).reduce(0, Integer::sum) < item.getForecastQty()) {
                                            //目标仓库产品库存不足
                                            throw new BizException(SysConstant.Target_Warehouse_Inventory_Shortage);
                                        } else {
                                            outProductIdMatchStockRack.put(item.getId(), productStockRackDoList);
                                        }
                                    }
                                    // 将原有库存释放
                                    List<OutboundPickingDo> pickingDoList = outboundPickingService.lambdaQuery()
                                            .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                                            .in(OutboundPickingDo::getOutboundProductId, outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                                            .list();
                                    if (CollUtil.isNotEmpty(pickingDoList)) {
                                        this.unLock(
                                                JSONUtil.toList(JSONUtil.toJsonStr(pickingDoList), OutboundPickingDo.class),
                                                pickingDoList,
                                                new AtomicInteger(pickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum))
                                        );
                                    }
                                    //锁定更新仓库的库存
                                    for (OrderOutboundProductDo item : outboundProductDoList) {
                                        //锁定库存并保存拣货记录
                                        this.lockStockAndSavePicking(
                                                outProductIdMatchStockRack.get(item.getId()),
                                                new AtomicInteger(item.getForecastQty()),
                                                String.valueOf(item.getProductId()),
                                                item.getId(),
                                                Boolean.FALSE
                                        );
                                    }
                                }
                            }
                            this.lambdaUpdate()
                                    .eq(OrderOutboundDo::getId, outboundDo.getId())
                                    .set(OrderOutboundDo::getWhId, warehouseDo.getId())
                                    .set(OrderOutboundDo::getSendOut, warehouseType == 1 && "DL01".equals(warehouseDo.getWhCode()) ? 1 : 0)
                                    .set(NormalOutboundStatusEnum.one.getValue().equals(outboundDo.getStatus()) && !outboundDo.getWhId().equals(warehouseDo.getId()), OrderOutboundDo::getPushWmsTime, new Date())
                                    .set(Objects.nonNull(finalLogisticsChannelDo), OrderOutboundDo::getLogisticsChannelId, Objects.nonNull(finalLogisticsChannelDo) ? finalLogisticsChannelDo.getId() : -1)
                                    .set(Objects.nonNull(finalLogisticsChannelDo), OrderOutboundDo::getCarrier, Objects.nonNull(finalLogisticsChannelDo) ? finalLogisticsChannelDo.getCarrier() : null)
                                    .update();
                            operateRecordService.operateAddRecordAndValue(
                                    SysConstant.OrderOutbound_Batch_Update_Wh_And_Channel,
                                    "warehouse[" + warehouseDo.getWhCode() + "]" + (Objects.nonNull(finalLogisticsChannelDo) ? ",channel[" + finalLogisticsChannelDo.getChannelCode() + "]" : ""),
                                    outboundDo.getId(),
                                    userId,
                                    username,
                                    OperateTypeEnum.OrderOutbound
                            );
                            return true;
                        } catch (Exception e) {
                            log.error("批量修改仓库失败outId[" + outboundDo.getId() + "]", e);
                            // 手动标记回滚
                            status.setRollbackOnly();
                            String msg = SysConstant.Try_Again_Later;
                            if (e instanceof BizException) {
                                msg = e.getMessage();
                            }
                            HashMap<String, Object> map = new HashMap<>(4);
                            map.put("code", outboundDo.getOutboundNo());
                            map.put("errorMsg", StringsUtil.createI18Message(msg));
                            vo.getFailedList().add(map);
                            vo.setFailed(vo.getFailed() + 1);
                            return false;
                        }
                    });
                    if (Boolean.TRUE.equals(executeStatus)) {
                        progressDoList.addAll(this.getProgress(outboundDo, username));
                        outIdList.add(outboundDo.getId());
                        vo.setSuccess(vo.getSuccess() + 1);
                    }
                } else {
                    throw new BizException(SysConstant.Outbound_Processing);
                }
            } catch (InterruptedException e) {
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", outboundDo.getOutboundNo());
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Outbound_Processing));
                vo.getFailedList().add(map);
                vo.setFailed(vo.getFailed() + 1);
            } finally {
                if (isLocked && lock.isHeldByCurrentThread()) {
                    lock.unlock(); // 解锁
                }
            }
        }
        //删除
        if (!outIdList.isEmpty()) {
            progressService.deleteByIds(outIdList, ProgressEnum.OUTBOUND.getTypeCode());
        }
        progressService.batchInsert(progressDoList);
        //修改ec订单的仓库
        List<String> platformOrderNos = new ArrayList<>();
        outboundDoList.stream().filter(i -> Objects.equals(OutboundOriginEnum.three.getValue(), i.getOrderOrigin()) && StringUtils.isNotBlank(i.getPlatformOrderNo()) && outIdList.contains(i.getId())).forEach(i -> {
            platformOrderNos.addAll(Arrays.asList(i.getPlatformOrderNo().split(",")));
        });
        rabbitTemplate.convertAndSend(RabbitQueueConstant.Platform_OrderUpdate_Whid, JSON.toJSONString(new UpdateWarehouseReq(platformOrderNos, bo.getWhId(), bo.getLogisticsChannelId(), companyId, userId)));
        return vo;
    }

    @Override
    public OutboundPreOperationVo transferOutboundPreOperation(List<Long> ids) {
        OutboundPreOperationVo vo = new OutboundPreOperationVo();
        Map<Long, OrderOutboundDo> idMatchDo = this.listByIds(ids).stream().collect(Collectors.toMap(OrderOutboundDo::getId, item -> item));
        for (Long id : ids) {
            OrderOutboundDo outboundDo = idMatchDo.get(id);
            //查询装箱信息
//            List<OutboundBoxDo> outboundBoxDoList = outboundBoxService.lambdaQuery()
//                    .eq(OutboundBoxDo::getOutboundId, id)
//                    .select(OutboundBoxDo::getOutboundId)
//                    .list();
//            if (CollUtil.isEmpty(outboundBoxDoList) && outboundDo.getOutboundMode().equals(1)) {
            if (ids.size() == 1) {
                vo.setHasNotBoxInfo(true);
                vo.setHasNotPalletInfo(true);
                vo.setHasNotBoxInfoOutboundId(id);
                vo.setLogisticsChannelId(outboundDo.getLogisticsChannelId());
                return vo;
            }
            //查询出库托盘信息
//            if (!vo.isHasNotPalletInfo()) {
//                Integer outboundPalletsNum = outboundDo.getOutboundPalletsNum();
//                if (Objects.isNull(outboundPalletsNum) || outboundPalletsNum == 0) {
//            vo.setHasNotPalletInfo(true);
//                }
//            }
        }
        return vo;
    }

    @Override
    public SingleResult transferOutboundOperationBatch(List<Long> ids, Long userId, String username) {
        CommonBatchOperationVo vo = new CommonBatchOperationVo();
        vo.setTotal(ids.size());
        Map<Long, OrderOutboundDo> idMatchDo = this.listByIds(ids).stream()
                .collect(Collectors.toMap(OrderOutboundDo::getId, item -> item));
        for (Long id : ids) {
            OrderOutboundDo outboundDo = idMatchDo.get(id);
            if (Objects.isNull(outboundDo)) {
                vo.setFailed(vo.getFailed() + 1);
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", id);
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Not_Found_Outbound_Fail));
                vo.getFailedList().add(map);
                continue;
            }
            if (Objects.nonNull(outboundDo.getMarkExceptionTime())) {
                vo.setFailed(vo.getFailed() + 1);
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", outboundDo.getOutboundNo());
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Marked_Exception_Outbound_Fail));
                vo.getFailedList().add(map);
                continue;
            }
            this.transferOutboundOperateAfterHandler(outboundDo, userId, username, null, null, null, null, false);
            vo.setSuccess(vo.getSuccess() + 1);
        }
        return SingleResult.success(vo);
    }

    @Override
    public SingleResult transferOutboundOperation(TransferOutboundOperationBo bo, Long userId, String username) {
        CommonBatchOperationVo vo = new CommonBatchOperationVo();
        vo.setTotal(1);
        OrderOutboundDo outboundDo = this.getById(bo.getId());
        if (Objects.isNull(outboundDo)) {
            vo.setFailed(vo.getFailed() + 1);
            HashMap<String, Object> map = new HashMap<>(4);
            map.put("code", bo.getId());
            map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Not_Found_Outbound_Fail));
            vo.getFailedList().add(map);
            return SingleResult.success(vo);
        }
        if (Objects.nonNull(outboundDo.getMarkExceptionTime())) {
            vo.setFailed(vo.getFailed() + 1);
            HashMap<String, Object> map = new HashMap<>(4);
            map.put("code", outboundDo.getOutboundNo());
            map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Marked_Exception_Outbound_Fail));
            vo.getFailedList().add(map);
            return SingleResult.success(vo);
        }
        this.transferOutboundOperateAfterHandler(outboundDo, userId, username, bo.getTrackingNumber(), bo.getOutboundPalletsNum(), bo.getLogisticsChannelId(), bo.getCustomizeChannelName(), false);
        vo.setSuccess(vo.getSuccess() + 1);
        return SingleResult.success(vo);
    }

    @Override
    public TransferOutboundPackDetail transferOutboundPackDetail(Long id) {
        OrderOutboundDo outboundDo = this.getById(id);
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.Outbound_Order_Does_Not_Exist);
        }
        if (!Objects.equals(outboundDo.getStatus(), TransferOutboundStatusEnum.three.getValue()) && !Objects.equals(outboundDo.getStatus(), TransferOutboundStatusEnum.four.getValue())) {
            throw new BizException(SysConstant.Status_Error);
        }
        TransferOutboundPackDetail outboundPackDetail = new TransferOutboundPackDetail();
        outboundPackDetail.setId(id);
        outboundPackDetail.setOutboundNo(outboundDo.getOutboundNo());
        outboundPackDetail.setOutboundMode(outboundDo.getOutboundMode());
        outboundPackDetail.setPalletNum(outboundDo.getOutboundPalletsNum());
        //出库产品信息
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, id)
                .list();
        Integer outboundMode = outboundDo.getOutboundMode();
        Map<Long, ProductDo> productIdMatchDo = new HashMap<>();
        Map<String, OrderBoxDo> boxTypeNoMatchDo = new HashMap<>();
        if (Objects.equals(outboundMode, 1)) {
            productIdMatchDo = productService.lambdaQuery()
                    .in(ProductDo::getId, outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toSet()))
                    .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        } else if (Objects.equals(outboundMode, 2)) {
            boxTypeNoMatchDo = boxService.lambdaQuery()
                    .in(OrderBoxDo::getBoxTypeNumber, outboundProductDoList.stream().map(OrderOutboundProductDo::getBoxTypeNumber).collect(Collectors.toSet()))
                    .list().stream().collect(Collectors.toMap(OrderBoxDo::getBoxTypeNumber, val -> val, (o1, o2) -> o1));
        }
        for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
            if (Objects.equals(outboundMode, 1)) {
                ProductDo productDo = productIdMatchDo.get(outboundProductDo.getProductId());
                if (productDo != null) {
//                    TransferOutboundPackDetail.ProductInfo productInfo = new TransferOutboundPackDetail.ProductInfo();
//                    BeanUtils.copyProperties(productDo, productInfo);
                    TransferOutboundPackDetail.ProductInfo productInfo = BeanConvert.INSTANCE.productDoToTransferOutboundPackProductInfo(productDo);
                    productInfo.setProductId(outboundProductDo.getProductId());
                    productInfo.setSkuOrBoxTypeNumber(productDo.getSku());
                    productInfo.setJanCodeOrItf(productDo.getJanCode());
                    productInfo.setQty(outboundProductDo.getOutboundQty());
                    if (productDo.getWmsLength() != null) {
                        productInfo.setLength(productDo.getWmsLength());
                        productInfo.setLengthBs(productDo.getWmsLengthBs());
                        productInfo.setWidth(productDo.getWmsWidth());
                        productInfo.setWidthBs(productDo.getWmsWidthBs());
                        productInfo.setHeight(productDo.getWmsHeight());
                        productInfo.setHeightBs(productDo.getWmsHeightBs());
                        productInfo.setWeight(productDo.getWmsWeight());
                        productInfo.setWeightBs(productDo.getWmsWeightBs());
                    }
                    outboundPackDetail.getProductInfoList().add(productInfo);
                }
            } else if (Objects.equals(outboundMode, 2)) {
                OrderBoxDo boxDo = boxTypeNoMatchDo.get(outboundProductDo.getBoxTypeNumber());
                if (boxDo != null) {
//                    TransferOutboundPackDetail.ProductInfo productInfo = new TransferOutboundPackDetail.ProductInfo();
//                    BeanUtils.copyProperties(boxDo, productInfo);
                    TransferOutboundPackDetail.ProductInfo productInfo = BeanConvert.INSTANCE.boxDoToTransferOutboundPackProductInfo(boxDo);
                    productInfo.setProductId(-1L);
                    productInfo.setSkuOrBoxTypeNumber(outboundProductDo.getBoxTypeNumber());
                    productInfo.setJanCodeOrItf(boxDo.getCustomizeBarcode());
                    productInfo.setQty(outboundProductDo.getOutboundQty());
                    outboundPackDetail.getProductInfoList().add(productInfo);
                }
            }
        }
        //查询包裹尺寸
        outboundPackDetail.setOutboundPackageList(getPackageListByOutboundId(id));
        return outboundPackDetail;
    }

    /**
     * 根据出库单id查包裹明细
     *
     * @param id 出库单id
     * @return 出库包裹明细 （包括包裹内产品信息、包裹包材、包裹面单）
     */
    private List<OutboundPackageDetail> getPackageListByOutboundId(Long id) {
        List<OrderOutboundPackageDo> packageDoList = outboundPackageService.lambdaQuery()
                .eq(OrderOutboundPackageDo::getOutboundId, id)
                .list();
        if (CollUtil.isEmpty(packageDoList)) {
            return Collections.emptyList();
        }
        Set<Long> packIdSet = packageDoList.stream().map(OrderOutboundPackageDo::getId).collect(Collectors.toSet());
        Map<Long, List<OrderOutboundPackageDetailDo>> packIdMatchPackDetails =
                outboundPackageDetailService.lambdaQuery()
                        .in(OrderOutboundPackageDetailDo::getPackageId, packIdSet)
                        .list().stream().collect(Collectors.groupingBy(OrderOutboundPackageDetailDo::getPackageId));
        Map<Long, List<OrderOutboundPackageMaterialDo>> packIdMatchPackMaterials =
                outboundPackageMaterialService.lambdaQuery()
                        .in(OrderOutboundPackageMaterialDo::getPackageId, packIdSet)
                        .list().stream().collect(Collectors.groupingBy(OrderOutboundPackageMaterialDo::getPackageId));
        List<Long> labelFileIdList = packageDoList.stream().map(OrderOutboundPackageDo::getLabelFileId).filter(Objects::nonNull).collect(Collectors.toList());
        Map<Long, FileVo> fileIdMatchFileVo = new HashMap<>();
        if (CollUtil.isNotEmpty(labelFileIdList)) {
            fileIdMatchFileVo = fileService.getFileVoByIds(labelFileIdList.stream().map(String::valueOf).collect(Collectors.joining(","))).stream().collect(Collectors.toMap(FileVo::getId, val -> val));
        }
        List<OutboundPackageDetail> outboundPackageList = new ArrayList<>();
        for (OrderOutboundPackageDo packageDo : packageDoList) {
//            OutboundPackageDetail packageDetail = new OutboundPackageDetail();
//            BeanUtils.copyProperties(packageDo, packageDetail);
            OutboundPackageDetail packageDetail = BeanConvert.INSTANCE.packageDoToPackageDetail(packageDo);
            //包裹明细
            List<OrderOutboundPackageDetailDo> packageDetailDoList = packIdMatchPackDetails.get(packageDo.getId());
            if (CollUtil.isNotEmpty(packageDetailDoList)) {
                packageDetail.setProducts(JSONUtil.toList(JSONUtil.toJsonStr(packageDetailDoList), OutboundPackProductVo.class));
            }
            //包裹包材
            List<OrderOutboundPackageMaterialDo> packageMaterialDoList = packIdMatchPackMaterials.get(packageDo.getId());
            if (CollUtil.isNotEmpty(packageMaterialDoList)) {
                packageDetail.setPackInfoVoList(JSONUtil.toList(JSONUtil.toJsonStr(packageMaterialDoList), PackInfoVo.class));
            }
            if (packageDo.getLabelFileId() != null) {
                packageDetail.setFileVo(fileIdMatchFileVo.getOrDefault(packageDo.getLabelFileId(), null));
            }
            outboundPackageList.add(packageDetail);
        }
        return outboundPackageList;
    }

    @Override
    public SingleResult transferOutboundPack(TransferOutboundPackBo bo, Long userId, String username) {
        OrderOutboundDo outboundDo = this.getById(bo.getId());
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.Outbound_Order_Does_Not_Exist);
        }
        if (!Objects.equals(outboundDo.getStatus(), TransferOutboundStatusEnum.three.getValue()) && !Objects.equals(outboundDo.getStatus(), TransferOutboundStatusEnum.four.getValue())) {
            throw new BizException(SysConstant.Status_Error);
        }
        //装箱信息
        List<TransferOutboundPackBo.PackageInfo> packInfoList = bo.getPackInfoList();
        if (CollUtil.isEmpty(packInfoList)) {
            throw new BizException(SysConstant.Order_NoPackage);
        }
        //清空装箱信息
        outboundPackageService.remove(Wrappers.<OrderOutboundPackageDo>lambdaQuery().eq(OrderOutboundPackageDo::getOutboundId, outboundDo.getId()));
        outboundPackageDetailService.remove(Wrappers.<OrderOutboundPackageDetailDo>lambdaQuery().eq(OrderOutboundPackageDetailDo::getOutboundId, outboundDo.getId()));
        //出库方式
        Integer outboundMode = outboundDo.getOutboundMode();
        //校验装箱数量和出库数量是否匹配
        Map<String, Integer> skuOrBoxNoPackQty = packInfoList.stream()
                .map(TransferOutboundPackBo.PackageInfo::getPackageDetailList)
                .flatMap(List::stream)
                .collect(Collectors.toMap(TransferOutboundPackBo.PackageDetail::getSkuOrBoxTypeNumber, TransferOutboundPackBo.PackageDetail::getQty, Integer::sum));
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, bo.getId())
                .list();
        Map<Long, ProductDo> productIdMatchDo = new HashMap<>();
        if (outboundMode.equals(1)) {
            productIdMatchDo = productService.lambdaQuery()
                    .in(ProductDo::getId, outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList()))
                    .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        } else if (outboundMode.equals(2)) {
            //根据箱号合并数据
            outboundProductDoList = new ArrayList<>(outboundProductDoList.stream().collect(Collectors.toMap(OrderOutboundProductDo::getBoxTypeNumber, val -> val, (o1, o2) -> o1)).values());
        }
        //
        List<Map<String, Object>> maps = new ArrayList<>();
        for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
            if (outboundMode.equals(1)) {
                ProductDo productDo = productIdMatchDo.get(outboundProductDo.getProductId());
                if (Objects.isNull(productDo)) {
                    continue;
                }
                Integer packQty = skuOrBoxNoPackQty.get(productDo.getSku());
                if (!Objects.equals(outboundProductDo.getOutboundQty(), packQty)) {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("SKU", productDo.getSku());
                    map.put("outboundQty", outboundProductDo.getOutboundQty());
                    map.put("packQty", Objects.nonNull(packQty) ? packQty : 0);
                    map.put("diff", outboundProductDo.getOutboundQty() - (Objects.nonNull(packQty) ? packQty : 0));
                    maps.add(map);
                }
            } else if (outboundMode.equals(2)) {
                Integer packQty = skuOrBoxNoPackQty.get(outboundProductDo.getBoxTypeNumber());
                if (!Objects.equals(outboundProductDo.getOutboundBoxQty(), packQty)) {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("SKU", outboundProductDo.getBoxTypeNumber());
                    map.put("outboundQty", outboundProductDo.getOutboundBoxQty());
                    map.put("packQty", Objects.nonNull(packQty) ? packQty : 0);
                    map.put("diff", outboundProductDo.getOutboundBoxQty() - (Objects.nonNull(packQty) ? packQty : 0));
                    maps.add(map);
                }
            }
        }
        if (CollUtil.isNotEmpty(maps)) {
            return SingleResult.success("出库数量和装箱数量存在差异", maps);
        }
        for (TransferOutboundPackBo.PackageInfo packageInfo : packInfoList) {
//            OrderOutboundPackageDo packageDo = new OrderOutboundPackageDo();
//            BeanUtils.copyProperties(packageInfo, packageDo);
            OrderOutboundPackageDo packageDo = BeanConvert.INSTANCE.packPackageInfoToPackageDo(packageInfo);
            packageDo.setOutboundId(bo.getId());
            //保存包裹信息
            outboundPackageService.save(packageDo);
            List<OrderOutboundPackageDetailDo> packageDetailDoList = new ArrayList<>();
            for (TransferOutboundPackBo.PackageDetail packageDetail : packageInfo.getPackageDetailList()) {
                OrderOutboundPackageDetailDo packageDetailDo = new OrderOutboundPackageDetailDo();
                packageDetailDo.setOutboundId(bo.getId());
                packageDetailDo.setPackageId(packageDo.getId());
                packageDetailDo.setProductId(packageDetail.getProductId());
                packageDetailDo.setSku(packageDetail.getSkuOrBoxTypeNumber());
                packageDetailDo.setQty(packageDetail.getQty());
                packageDetailDoList.add(packageDetailDo);
            }
            //保存包裹明细
            outboundPackageDetailService.saveBatch(packageDetailDoList);
        }
        String trackingNumberStr = packInfoList.stream().map(TransferOutboundPackBo.PackageInfo::getTrackingNumber).filter(StrUtil::isNotBlank).collect(Collectors.joining(","));
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, bo.getId())
                .set(Objects.nonNull(bo.getPalletNum()), OrderOutboundDo::getOutboundPalletsNum, bo.getPalletNum())
                .set(StrUtil.isNotBlank(trackingNumberStr), OrderOutboundDo::getTrackingNumber, trackingNumberStr)
                .set(OrderOutboundDo::getPackageNum, packInfoList.size())
                .update();
        operateRecordService.operateAddRecordAndValue(
                SysConstant.OrderOutbound_Pack_Update,
                "",
                bo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        return SingleResult.success();
    }

    @Override
    public SingleResult normalCreateWave(NormalOutboundCreateWaveBo bo, Long userId, String username) {
        if (CollUtil.isEmpty(bo.getOutboundIds())) {
            throw new BizException(SysConstant.No_Data);
        }
        if (bo.getOutboundIds().size() > 1000) {
            throw new BizException(SysConstant.Batch_Over_1000);
        }
        List<OrderOutboundDo> outboundDoList = this.lambdaQuery()
                .in(OrderOutboundDo::getId, bo.getOutboundIds())
                .eq(OrderOutboundDo::getStatus, TransferOutboundStatusEnum.one.getValue())
                .list();
        if (CollUtil.isEmpty(outboundDoList)) {
            throw new BizException(SysConstant.Create_Wave_Status_Error);
        }
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .list();
        OutboundWaveDo waveDo = new OutboundWaveDo();
        waveDo.setWaveNo(this.generateWaveNo());
        waveDo.setStatus(WaveStatusEnum.zero.getValue());
        waveDo.setShouldPickingQty(outboundProductDoList.stream().mapToInt(OrderOutboundProductDo::getForecastQty).sum());
        waveDo.setWaveVarietyType(outboundDoList.size() > 1 ? 2 : 1);
        waveDo.setOrderNum(outboundDoList.size());
        if (Objects.nonNull(bo.getPicker())) {
            waveDo.setPicker(bo.getPicker());
        }
        //保存波次
        waveService.save(waveDo);
        for (OrderOutboundDo outboundDo : outboundDoList) {
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.OrderOutbound_Create_Wave,
                    "",
                    outboundDo.getId(),
                    userId,
                    username,
                    OperateTypeEnum.OrderOutbound
            );
            progressService.lambdaUpdate()
                    .eq(ProgressDo::getBizNumber, outboundDo.getId())
                    .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                    .eq(ProgressDo::getBizType, "生成波次")
                    .set(ProgressDo::getOperateBy, username)
                    .set(ProgressDo::getOperateTime, DateUtil.date())
                    .update();
            //修改订单生成波次时间、状态、波次id
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, outboundDo.getId())
                    .set(OrderOutboundDo::getCreateWaveTime, DateUtil.date())
                    .set(OrderOutboundDo::getStatus, NormalOutboundStatusEnum.two.getValue())
                    .set(OrderOutboundDo::getWaveId, waveDo.getId())
                    .update();
        }
        HashMap<String, Object> map = new HashMap<>(4);
        map.put("id", waveDo.getId());
        map.put("waveNo", waveDo.getWaveNo());
        operateRecordService.operateAddRecordAndValue(
                SysConstant.OrderOutbound_Create_Wave,
                "",
                waveDo.getId(),
                userId,
                username,
                OperateTypeEnum.Wave
        );
        return SingleResult.success("生成波次成功", map);
    }

    @Override
    public SingleResult normalWeighDetail(String scan) {
        if (StrUtil.isBlank(scan)) {
            throw new BizException(SysConstant.Scan_Empty);
        }
        OrderOutboundDo outboundDo = this.lambdaQuery()
                .eq(OrderOutboundDo::getOutboundNo, scan)
                .one();
        if (Objects.isNull(outboundDo)) {
            outboundDo = this.lambdaQuery()
                    .apply("find_in_set({0},tracking_number)", scan)
                    .orderByDesc(OrderOutboundDo::getCreateTime)
                    .last("limit 1")
                    .one();
        }
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.Scan_OutboundNo_Error);
        }
        if (OutboundTypeEnum.one.getValue().equals(outboundDo.getOrderType()) && !Objects.equals(NormalOutboundStatusEnum.three.getValue(), outboundDo.getStatus())) {
            throw new BizException(SysConstant.Normal_Outbound_Non_Recheck_Status);
        }
        if (OutboundTypeEnum.two.getValue().equals(outboundDo.getOrderType()) && !Objects.equals(TransferOutboundStatusEnum.three.getValue(), outboundDo.getStatus())) {
            throw new BizException(SysConstant.Transfer_Outbound_Non_To_Be_Shipped_Status);
        }
        NormalWeighDetail detail = new NormalWeighDetail();
        detail.setId(outboundDo.getId());
        detail.setOutboundNo(outboundDo.getOutboundNo());
        if (Objects.nonNull(outboundDo.getLogisticsChannelId())) {
            LogisticsChannelDo channelDo = logisticsChannelService.getById(outboundDo.getLogisticsChannelId());
            if (Objects.nonNull(channelDo)) {
                detail.setLogisticsChannel(channelDo.getChannelName());
            } else {
                detail.setLogisticsChannel("自动选取价格最低的物流渠道");
            }
        }
        detail.setWeighTime(outboundDo.getWeighTime());
        List<OrderOutboundPackageDo> packageDoList = outboundPackageService.lambdaQuery()
                .eq(OrderOutboundPackageDo::getOutboundId, outboundDo.getId())
                .list();
        if (CollUtil.isNotEmpty(packageDoList)) {
            Map<Long, List<OrderOutboundPackageMaterialDo>> materialMatch = outboundPackageMaterialService.lambdaQuery()
                    .in(OrderOutboundPackageMaterialDo::getPackageId, packageDoList.stream().map(OrderOutboundPackageDo::getId).collect(Collectors.toList()))
                    .list().stream().collect(Collectors.groupingBy(OrderOutboundPackageMaterialDo::getPackageId));
//            List<Long> labelFileIdList = packageDoList.stream().map(OrderOutboundPackageDo::getLabelFileId).filter(Objects::nonNull).collect(Collectors.toList());
//            Map<Long, FileVo> fileIdMatchFileVo = new HashMap<>();
//            if (CollUtil.isNotEmpty(labelFileIdList)) {
//                fileIdMatchFileVo = fileService.getFileVoByIds(labelFileIdList.stream().map(String::valueOf).collect(Collectors.joining(","))).stream().collect(Collectors.toMap(FileVo::getId, val -> val));
//            }
            for (OrderOutboundPackageDo packageDo : packageDoList) {
//                OutboundPackageDetail packageDetail = new OutboundPackageDetail();
//                BeanUtils.copyProperties(packageDo, packageDetail);
                OutboundPackageDetail packageDetail = BeanConvert.INSTANCE.packageDoToPackageDetail(packageDo);
                if (Objects.isNull(packageDetail.getSerialNumber())) {
                    packageDetail.setSerialNumber(1);
                }
                List<OrderOutboundPackageMaterialDo> materialDos = materialMatch.get(packageDo.getId());
                if (CollUtil.isNotEmpty(materialDos)) {
                    List<PackInfoVo> packInfoVos = JSONUtil.toList(JSONUtil.toJsonStr(materialDos), PackInfoVo.class);
                    packageDetail.setPackInfoVoList(packInfoVos);
                }
//                if (packageDo.getLabelFileId() != null) {
//                    packageDetail.setFileVo(fileIdMatchFileVo.getOrDefault(packageDo.getLabelFileId(), null));
//                }
                detail.getOutboundPackageList().add(packageDetail);
            }
        }
        return SingleResult.success(detail);
    }

    @Override
    public SingleResult weigh(NormalWeighBo bo, Long userId, String username, Long whId) {
        OrderOutboundDo outboundDo = this.lambdaQuery()
                .eq(OrderOutboundDo::getOutboundNo, bo.getOutboundNo())
                .one();
        if (OutboundTypeEnum.one.getValue().equals(outboundDo.getOrderType()) && Objects.equals(NormalOutboundStatusEnum.five.getValue(), outboundDo.getStatus())) {
            throw new BizException(SysConstant.Outbound_Order_Already_Outbound);
        }
        if (OutboundTypeEnum.two.getValue().equals(outboundDo.getOrderType()) && Objects.equals(TransferOutboundStatusEnum.four.getValue(), outboundDo.getStatus())) {
            throw new BizException(SysConstant.Outbound_Order_Already_Outbound);
        }
        //修改出库单称重时间
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, outboundDo.getId())
                .set(OrderOutboundDo::getWeighTime, DateUtil.date())
                .update();
        operateRecordService.operateAddRecordAndValue(
                SysConstant.OrderOutbound_Weigh,
                "",
                outboundDo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        List<OrderOutboundPackageDo> packageDoList = outboundPackageService.lambdaQuery()
                .eq(OrderOutboundPackageDo::getOutboundId, outboundDo.getId())
                .list();
        Map<Integer, OutboundPackageDetail> seqMatchPackage = bo.getOutboundPackageList().stream().collect(Collectors.toMap(OutboundPackageDetail::getSerialNumber, val -> val));
        for (OrderOutboundPackageDo packageDo : packageDoList) {
            List<OrderOutboundPackageMaterialDo> materialDos = null;
            OutboundPackageDetail packageDetail = seqMatchPackage.get(packageDo.getSerialNumber());
            List<PackInfoVo> packInfoVoList = packageDetail.getPackInfoVoList();
            outboundPackageMaterialService.remove(
                    Wrappers.<OrderOutboundPackageMaterialDo>lambdaQuery().eq(
                            OrderOutboundPackageMaterialDo::getPackageId, packageDo.getId()));
            if (CollUtil.isNotEmpty(packInfoVoList)) {
                //修改包材
                materialDos = JSONUtil.toList(JSONUtil.toJsonStr(packInfoVoList), OrderOutboundPackageMaterialDo.class);
                materialDos.forEach(materialDo -> materialDo.setPackageId(packageDo.getId()));
                outboundPackageMaterialService.saveBatch(materialDos);
            }
            if (CollUtil.isEmpty(materialDos)) {
                materialDos = outboundPackageMaterialService.lambdaQuery()
                        .eq(OrderOutboundPackageMaterialDo::getPackageId, packageDo.getId())
                        .list();
            }
            switch (bo.getWeighType()) {
                case 1:
                    packageDo.setWeight(packageDetail.getWeight());
                    packageDo.setWeightBs(packageDetail.getWeight().multiply(BigDecimal.valueOf(2.2046)).setScale(3, RoundingMode.HALF_UP));
                    break;
                case 2:
                    //自动计算重量 加上包材重量
                    if (CollUtil.isNotEmpty(materialDos)) {
                        Map<String, BigDecimal> materialMatchWeight = materialService.lambdaQuery()
                                .eq(MaterialDo::getWhId, whId)
                                .in(MaterialDo::getMaterialCode, materialDos.stream().map(OrderOutboundPackageMaterialDo::getMaterialCode).collect(Collectors.toList()))
                                .list().stream().collect(Collectors.toMap(MaterialDo::getMaterialCode, MaterialDo::getWeight));
                        packageDo.setWeight(packageDo.getWeight().add(materialDos.stream()
                                .map(item -> materialMatchWeight.get(item.getMaterialCode())
                                        .multiply(BigDecimal.valueOf(item.getMaterialNum())))
                                .reduce(BigDecimal.ZERO, BigDecimal::add)));
                        packageDo.setWeightBs(packageDo.getWeight().multiply(BigDecimal.valueOf(2.2046)).setScale(3, RoundingMode.HALF_UP));
                    }
                    break;
                default:
            }
            if (Objects.nonNull(packageDetail.getLength())) {
                packageDo.setLength(packageDetail.getLength());
                packageDo.setLengthBs(packageDetail.getLength().divide(BigDecimal.valueOf(2.54), 3, RoundingMode.HALF_UP));
            }
            if (Objects.nonNull(packageDetail.getWidth())) {
                packageDo.setWidth(packageDetail.getWidth());
                packageDo.setWidthBs(packageDetail.getWidth().divide(BigDecimal.valueOf(2.54), 3, RoundingMode.HALF_UP));
            }
            if (Objects.nonNull(packageDetail.getHeight())) {
                packageDo.setHeight(packageDetail.getHeight());
                packageDo.setHeightBs(packageDetail.getHeight().divide(BigDecimal.valueOf(2.54), 3, RoundingMode.HALF_UP));
            }
            outboundPackageService.updateById(packageDo);
        }
        if (bo.isImmediate()) {
            Integer orderType = outboundDo.getOrderType();
            if (Objects.equals(orderType, OutboundTypeEnum.one.getValue())) {
                this.normalOutboundOperateAfterHandler(outboundDo, userId, username, SysConstant.Quick_Outbound);
            }
            if (Objects.equals(orderType, OutboundTypeEnum.two.getValue())) {
                this.transferOutboundOperateAfterHandler(outboundDo, userId, username, null, null, null, null, false);
            }
            if (Objects.equals(outboundDo.getOrderOrigin(), OutboundOriginEnum.three.getValue())
                && StringUtils.isNotEmpty(outboundDo.getPlatformOrderNo())) {
                PlatOrderSendConsumerReq req = new PlatOrderSendConsumerReq(Collections.singletonList(
                        new PlatformOrderTrackNoEntity(outboundDo.getId(), outboundDo.getPlatformOrderNo(),
                                outboundDo.getLogisticsChannelId(), outboundDo.getCarrier(), outboundDo.getTrackingNumber())), userId, outboundDo.getCompanyId());
                rabbitTemplate.convertAndSend(RabbitQueueConstant.PlatformOrder_Finish, JSON.toJSONString(req));
            }
        }
        return SingleResult.success();
    }

    @Override
    public SingleResult normalQuickOutbound(List<Long> ids, Long userId, String username) {
        if (CollUtil.isEmpty(ids)) {
            throw new BizException(SysConstant.No_Data);
        }
        CommonBatchOperationVo vo = new CommonBatchOperationVo();
        vo.setTotal(ids.size());
        Map<Long, OrderOutboundDo> idMatchDo = this.listByIds(ids).stream()
                .collect(Collectors.toMap(OrderOutboundDo::getId, item -> item));
        List<PlatOrderSendConsumerReq> success = new ArrayList<>();
        for (Long id : ids) {
            OrderOutboundDo outboundDo = idMatchDo.get(id);
            if (Objects.isNull(outboundDo)) {
                vo.setFailed(vo.getFailed() + 1);
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", id);
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Not_Found_Outbound_Fail));
                vo.getFailedList().add(map);
                continue;
            }
            if (Objects.nonNull(outboundDo.getMarkExceptionTime())) {
                vo.setFailed(vo.getFailed() + 1);
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", outboundDo.getOutboundNo());
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Marked_Exception_Outbound_Fail));
                vo.getFailedList().add(map);
                continue;
            }
            progressService.remove(
                    Wrappers.<ProgressDo>lambdaQuery()
                            .eq(ProgressDo::getBizNumber, outboundDo.getId())
                            .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                            .eq(ProgressDo::getBizType, "复核")
            );
            this.normalOutboundOperateAfterHandler(outboundDo, userId, username, SysConstant.Quick_Outbound);
            vo.setSuccess(vo.getSuccess() + 1);
            if (Objects.equals(outboundDo.getOrderOrigin(), OutboundOriginEnum.three.getValue())
                && StringUtils.isNotEmpty(outboundDo.getPlatformOrderNo())) {
                PlatOrderSendConsumerReq req = new PlatOrderSendConsumerReq(Collections.singletonList(
                        new PlatformOrderTrackNoEntity(outboundDo.getId(), outboundDo.getPlatformOrderNo(),
                                outboundDo.getLogisticsChannelId(), outboundDo.getCarrier(), outboundDo.getTrackingNumber())), userId, outboundDo.getCompanyId());
//                rabbitTemplate.convertAndSend(RabbitQueueConstant.PlatformOrder_Finish, JSON.toJSONString(req));
                success.add(req);
            }
        }
        context.publishEvent(new PlatformOrderSendEvent(success, userId));
        return SingleResult.success(vo);
    }

    @Override
    public SingleResult normalOutboundOperate(List<Long> ids, Long userId, String username) {
        if (CollUtil.isEmpty(ids)) {
            throw new BizException(SysConstant.No_Data);
        }
        CommonBatchOperationVo vo = new CommonBatchOperationVo();
        vo.setTotal(ids.size());
        Map<Long, OrderOutboundDo> idMatchDo = this.listByIds(ids).stream()
                .collect(Collectors.toMap(OrderOutboundDo::getId, item -> item));
        List<PlatOrderSendConsumerReq> success = new ArrayList<>();
        for (Long id : ids) {
            OrderOutboundDo outboundDo = idMatchDo.get(id);
            if (Objects.isNull(outboundDo)) {
                vo.setFailed(vo.getFailed() + 1);
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", id);
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Not_Found_Outbound_Fail));
                vo.getFailedList().add(map);
                continue;
            }
            if (Objects.nonNull(outboundDo.getMarkExceptionTime())) {
                vo.setFailed(vo.getFailed() + 1);
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", outboundDo.getOutboundNo());
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Marked_Exception_Outbound_Fail));
                vo.getFailedList().add(map);
                continue;
            }
            this.normalOutboundOperateAfterHandler(outboundDo, userId, username, SysConstant.OrderOutbound_Normal_Outbound);
            vo.setSuccess(vo.getSuccess() + 1);
            if (Objects.equals(outboundDo.getOrderOrigin(), OutboundOriginEnum.three.getValue())
                && StringUtils.isNotEmpty(outboundDo.getPlatformOrderNo())) {
                PlatOrderSendConsumerReq req = new PlatOrderSendConsumerReq(Collections.singletonList(
                        new PlatformOrderTrackNoEntity(outboundDo.getId(), outboundDo.getPlatformOrderNo(),
                                outboundDo.getLogisticsChannelId(), outboundDo.getCarrier(), outboundDo.getTrackingNumber())), userId, outboundDo.getCompanyId());
//                rabbitTemplate.convertAndSend(RabbitQueueConstant.PlatformOrder_Finish, JSON.toJSONString(req));
                success.add(req);
            }
        }
        context.publishEvent(new PlatformOrderSendEvent(success, userId));
        return SingleResult.success(vo);
    }

    @Override
    public SingleResult markException(Long id, Long userId, String username) {
        OrderOutboundDo outboundDo = this.getById(id);
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.No_Data);
        }
        if (Objects.nonNull(outboundDo.getMarkExceptionTime())) {
            throw new BizException(SysConstant.Order_Already_Mark_Exception);
        }
        if (outboundDo.getInterceptionFlag()) {
            throw new BizException(SysConstant.OutboundOrder_Already_Interception);
        }
        operateRecordService.operateAddRecordAndValue(
                SysConstant.OrderOutbound_Mark_Exception,
                "",
                outboundDo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, outboundDo.getId())
                .set(OrderOutboundDo::getMarkExceptionTime, DateUtil.date())
                .update();
        Integer orderType = outboundDo.getOrderType();
        if (orderType == 1) {
            //处理波次信息
            Long waveId = outboundDo.getWaveId();
            OutboundWaveDo waveDo = Objects.nonNull(waveId) ? waveService.getById(waveId) : null;
            if (Objects.nonNull(waveDo)) {
                if (waveDo.getOrderNum() == 1) {
                    waveService.removeById(waveId);
                } else {
                    waveDo.setOrderNum(waveDo.getOrderNum() - 1);
                    if (Objects.equals(waveDo.getOrderNum(), waveDo.getOutboundOrder())) {
                        waveDo.setOutbound(Boolean.TRUE);
                        waveDo.setOutboundTime(DateUtil.date());
                    }
                    List<Long> outboundIdList = this.lambdaQuery()
                            .eq(OrderOutboundDo::getWaveId, waveId)
                            .ne(OrderOutboundDo::getId, outboundDo.getId())
                            .list().stream().map(OrderOutboundDo::getId).collect(Collectors.toList());
                    List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                            .in(OrderOutboundProductDo::getOutboundId, outboundIdList)
                            .list();
                    Integer noRecheckNum = outboundProductDoList.stream().map(item -> item.getOutboundQty() - item.getRecheckQty()).reduce(0, Integer::sum);
                    if (noRecheckNum == 0) {
                        waveDo.setRecheck(Boolean.TRUE);
                        waveDo.setRecheckTime(DateUtil.date());
                    }
                    Integer noSecondarySortingNum = outboundProductDoList.stream().map(item -> item.getOutboundQty() - item.getSecondarySortingQty()).reduce(0, Integer::sum);
                    if (noSecondarySortingNum == 0) {
                        waveDo.setSecondarySorting(Boolean.TRUE);
                        waveDo.setSecondarySortingTime(DateUtil.date());
                    }
                    waveDo.setShouldPickingQty(waveDo.getShouldPickingQty() - outboundDo.getTotalNum());
                    waveDo.setPickingQty(outboundProductDoList.stream().map(OrderOutboundProductDo::getOutboundQty).reduce(0, Integer::sum));
                    waveDo.setWaveVarietyType(outboundProductDoList.size() > 1 ? 2 : 1);
                    waveService.updateById(waveDo);
                }
                //重置出库单波次
                this.lambdaUpdate()
                        .eq(OrderOutboundDo::getId, outboundDo.getId())
                        .set(OrderOutboundDo::getWaveId, null)
                        .update();
                List<Long> outboundProductIdList = outboundProductService.lambdaQuery()
                        .eq(OrderOutboundProductDo::getOutboundId, outboundDo.getId())
                        .list().stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList());
                //重置拣货状态
                outboundPickingService.lambdaUpdate()
                        .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                        .in(OutboundPickingDo::getOutboundProductId, outboundProductIdList)
                        .set(OutboundPickingDo::getPickFlag, Boolean.FALSE)
                        .update();
                //重置二次分拣、复核数量
                outboundProductService.lambdaUpdate()
                        .in(OrderOutboundProductDo::getId, outboundProductIdList)
                        .set(OrderOutboundProductDo::getRecheckQty, 0)
                        .set(OrderOutboundProductDo::getSecondarySortingQty, 0)
                        .update();
            }
        }
        return SingleResult.success();
    }

    @Override
    public PageResult exceptionOrderPageList(ExceptionOutboundQueryBo bo) {
        Integer current = bo.getCurrent();
        Integer pageSize = bo.getPageSize();
        PageHelper.startPage(current, pageSize);
        List<ExceptionOutboundVo> list = baseMapper.getExceptionOutboundListPage(bo);
        PageInfo<ExceptionOutboundVo> page = new PageInfo<>(list);
        return new PageResult<>(this.handlerExceptionListAfter(list), current, pageSize, (int) page.getTotal(), page.getPages());
    }

    /**
     * 查询分页列表之后对列表数据的处理
     */
    private List<ExceptionOutboundVo> handlerExceptionListAfter(List<ExceptionOutboundVo> list) {
        if (CollUtil.isEmpty(list)) {
            return list;
        }
        Map<Long, List<OutboundProductVo>> orderIdMatchProduct = baseMapper.batchGetOutboundSkuInfo(list.stream().map(ExceptionOutboundVo::getId).collect(Collectors.toList())).stream().collect(Collectors.groupingBy(OutboundProductVo::getOrderId));
        for (ExceptionOutboundVo vo : list) {
            vo.setProductDtoList(orderIdMatchProduct.getOrDefault(vo.getId(), new ArrayList<>()));
        }
        return list;
    }

    @Override
    public SingleResult removeException(Long id, Long userId, String username) {
        OrderOutboundDo outboundDo = this.getById(id);
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.No_Data);
        }
        Integer orderType = outboundDo.getOrderType();
        operateRecordService.operateAddRecordAndValue(
                SysConstant.OrderOutbound_Remove_Exception,
                "",
                outboundDo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, outboundDo.getId())
                .set(OrderOutboundDo::getMarkExceptionTime, null)
                .update();
        if (orderType == 1) {
            //一件代发出库单
            // 重置到待处理状态
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, outboundDo.getId())
                    .set(OrderOutboundDo::getStatus, NormalOutboundStatusEnum.one.getValue())
                    .set(OrderOutboundDo::getCreateWaveTime, null)
                    .set(OrderOutboundDo::getPickingTime, null)
                    .set(OrderOutboundDo::getRecheckTime, null)
                    .set(OrderOutboundDo::getWeighTime, null)
                    .update();
            progressService.lambdaUpdate()
                    .eq(ProgressDo::getBizNumber, id)
                    .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                    .notIn(ProgressDo::getBizType, "提交", "创建")
                    .set(ProgressDo::getOperateBy, null)
                    .set(ProgressDo::getOperateTime, null)
                    .update();
        }
        return SingleResult.success();
    }

    @Override
    public SingleResult exceptionConfirm(ExceptionConfirmBo bo, Long userId, String username) {
        OrderOutboundDo outboundDo = this.getById(bo.getId());
        if (Objects.isNull(outboundDo)) {
            throw new BizException(SysConstant.No_Data);
        }
        operateRecordService.operateAddRecordAndValue(
                SysConstant.OrderOutbound_Confirm_Exception,
                bo.getExceptionReason(),
                outboundDo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        List<ProgressDo> progressDoList = progressService.lambdaQuery()
                .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                .eq(ProgressDo::getBizNumber, outboundDo.getId())
                .list();
        if (CollUtil.isNotEmpty(progressDoList)) {
            for (ProgressDo progressDo : progressDoList.stream().sorted(Comparator.comparing(ProgressDo::getSort)).collect(Collectors.toList())) {
                if (Objects.isNull(progressDo.getOperateTime())) {
                    progressService.lambdaUpdate()
                            .eq(ProgressDo::getId, progressDo.getId())
                            .set(ProgressDo::getBizType, "异常")
                            .set(ProgressDo::getOperateBy, username)
                            .set(ProgressDo::getOperateTime, DateUtil.date())
                            .update();
                    break;
                }
            }
            progressService.remove(
                    Wrappers.<ProgressDo>lambdaQuery()
                            .eq(ProgressDo::getBizNumber, outboundDo.getId())
                            .isNull(ProgressDo::getOperateTime)
            );
        }
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, outboundDo.getId())
                .set(OrderOutboundDo::getStatus, (outboundDo.getOrderType() == 1 ? NormalOutboundStatusEnum.six.getValue() : TransferOutboundStatusEnum.five.getValue()))
                .set(OrderOutboundDo::getExceptionReason, bo.getExceptionReason())
                .set(OrderOutboundDo::getExceptionType, StringsUtil.createI18Message(StaticDict.Order_Outbound_ExceptionType.One.getValue()))
                .set(OrderOutboundDo::getExceptionTime, DateUtil.date())
                .update();
        //锁定库存需要释放
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, outboundDo.getId())
                .list();
        if (CollUtil.isNotEmpty(outboundProductDoList)) {
            List<OutboundPickingDo> pickingDoList = outboundPickingService.lambdaQuery()
                    .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                    .in(OutboundPickingDo::getOutboundProductId, outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                    .list();
            if (CollUtil.isNotEmpty(pickingDoList)) {
                this.unLock(
                        JSONUtil.toList(JSONUtil.toJsonStr(pickingDoList), OutboundPickingDo.class),
                        pickingDoList,
                        new AtomicInteger(pickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum))
                );
            }
        }
        return SingleResult.success();
    }

    /**
     * 中转出库单出库后置操作
     *
     * @param outboundDo
     * @param userId
     * @param username
     */
    @Transactional(rollbackFor = Exception.class)
    public void transferOutboundOperateAfterHandler(OrderOutboundDo outboundDo, Long userId, String username, String trackingNumber, Integer outboundPalletsNum, Long logisticsChannelId, String customizeChannelName, boolean apiInvoke) {
        operateRecordService.operateAddRecordAndValue(
                SysConstant.OrderOutbound_Confirm_Outbound,
                "",
                outboundDo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        progressService.lambdaUpdate()
                .eq(ProgressDo::getBizNumber, outboundDo.getId())
                .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                .eq(ProgressDo::getBizType, "出库")
                .set(ProgressDo::getOperateBy, username)
                .set(ProgressDo::getOperateTime, DateUtil.date())
                .update();
        LogisticsChannelDo logisticsChannelDo = null;
        if (Objects.nonNull(logisticsChannelId)) {
            logisticsChannelDo = logisticsChannelService.getById(logisticsChannelId);
        }
        if (StrUtil.isNotBlank(trackingNumber)) {
            String[] trackNoArr = trackingNumber.split(",");
            for (int i = 0; i < trackNoArr.length; i++) {
                outboundPackageService.lambdaUpdate()
                        .eq(OrderOutboundPackageDo::getOutboundId, outboundDo.getId())
                        .eq(OrderOutboundPackageDo::getSerialNumber, i + 1)
                        .set(OrderOutboundPackageDo::getTrackingNumber, trackNoArr[i])
                        .update();
            }
        }
        //提交时间
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, outboundDo.getId())
                .set(OrderOutboundDo::getOutboundTime, DateUtil.date())
                .set(OrderOutboundDo::getStatus, TransferOutboundStatusEnum.four.getValue())
                .set(Objects.nonNull(outboundPalletsNum), OrderOutboundDo::getOutboundPalletsNum, outboundPalletsNum)
                .set(StrUtil.isNotBlank(trackingNumber), OrderOutboundDo::getTrackingNumber, trackingNumber)
                .set(Objects.nonNull(logisticsChannelDo), OrderOutboundDo::getLogisticsChannelId, Objects.nonNull(logisticsChannelDo) ? logisticsChannelDo.getId() : null)
                .set(Objects.nonNull(logisticsChannelDo), OrderOutboundDo::getCarrier, Objects.nonNull(logisticsChannelDo) ? logisticsChannelDo.getCarrier() : null)
                .set(StrUtil.isNotBlank(customizeChannelName), OrderOutboundDo::getCustomizeChannelName, customizeChannelName)
                .update();
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, outboundDo.getId())
                .list();
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //锁定的库存扣除
        List<Long> outProductIdList = outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList());
        List<OutboundPickingDo> pickingDoList = outboundPickingService.lambdaQuery()
                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                .in(OutboundPickingDo::getOutboundProductId, outProductIdList)
                .eq(!apiInvoke, OutboundPickingDo::getPickFlag, Boolean.TRUE)
                .list();
        //查询库位信息 用于匹配库存属性
        StorageLocationQueryBo slBo = new StorageLocationQueryBo();
        slBo.setIdList(pickingDoList.stream().map(OutboundPickingDo::getRackId).collect(Collectors.toSet()));
        slBo.setTenantId(outboundDo.getTenantId());
        slBo.setPageSize(1000);
        Map<Long, StorageLocationVo> cellIdMatchDo = storageLocationService.getListPage(slBo).getData()
                .stream().collect(Collectors.toMap(StorageLocationVo::getId, val -> val));
        Integer outboundMode = outboundDo.getOutboundMode();
        String batchNo = DateUtils.dateFormatToString(outboundDo.getPushWmsTime(), "yyyyMMdd");
        for (OutboundPickingDo pickingDo : pickingDoList.stream().sorted(Comparator.comparing(OutboundPickingDo::getSubStockId)).collect(Collectors.toList())) {
            productStockRackService.lambdaUpdate()
                    .eq(ProductStockRackDo::getId, pickingDo.getSubStockId())
                    .setSql("lock_quantity = lock_quantity - " + pickingDo.getQty())
                    .update();
            Optional<OrderOutboundProductDo> first = outboundProductDoList.stream()
                    .filter(item -> Objects.equals(item.getId(), pickingDo.getOutboundProductId()))
                    .findFirst();
            if (first.isPresent()) {
                OrderOutboundProductDo outboundProductDo = first.get();
                //库存流水
                stockFlowingService.insertProForOut(
                        ProductFlowingOutEt.builder()
                                .sku(
                                        Objects.equals(outboundMode, 1)
                                                ? productIdMatch.get(outboundProductDo.getProductId()).getSku()
                                                : outboundProductDo.getBoxTypeNumber()
                                )
                                .companyId(outboundDo.getCompanyId())
                                .warehouseId(outboundDo.getWhId())
                                .rackId(pickingDo.getRackId())
                                .stockAttribute(cellIdMatchDo.get(pickingDo.getRackId()).getMainType())
                                .bookQuantity(-pickingDo.getQty())
                                .orderType(StockOrderTypeEnum.Outbound.getValue())
                                .noteNo(outboundDo.getOutboundNo())
                                .innerBatchNo(batchNo)
                                .tenantId(outboundDo.getTenantId())
                                .type(Objects.equals(outboundMode, 1) ? StockTypeEnum.one.getValue() : StockTypeEnum.two.getValue())
                                .build()
                );
            }
            outboundPickingService.removeById(pickingDo.getId());
        }
        //忽略进行中的截单，直接出库
        boolean isCancel = cancelService.lambdaUpdate()
                .eq(OutboundCancelDo::getOrderKind, OutboundOrderKind.outbound.getValue())
                .eq(OutboundCancelDo::getOutboundId, outboundDo.getId())
                .eq(OutboundCancelDo::getStatus, 0)
                .set(OutboundCancelDo::getStatus, 2)
                .set(OutboundCancelDo::getFinishTime, DateUtil.date())
                .update();
        if (isCancel) {
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.Order_Interception_Fail,
                    "",
                    outboundDo.getId(),
                    userId,
                    username,
                    OperateTypeEnum.OrderOutbound
            );
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, outboundDo.getId())
                    .set(OrderOutboundDo::getInterceptionFlag, Boolean.FALSE)
                    .update();
        }
//        rabbitTemplate.convertAndSend(RabbitQueueConstant.Outbound_Real_Cost, JSON.toJSONString(outboundDo));
        context.publishEvent(new OutboundAddCostEvent(outboundDo, 0));
    }

    @Transactional(rollbackFor = Exception.class)
    public void normalOutboundOperateAfterHandler(OrderOutboundDo outboundDo, Long userId, String username, String operateKey) {
        if (Objects.equals(outboundDo.getStatus(), NormalOutboundStatusEnum.five.getValue())) {
            return;
        }
        operateRecordService.operateAddRecordAndValue(
                operateKey,
                "",
                outboundDo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        progressService.lambdaUpdate()
                .eq(ProgressDo::getBizNumber, outboundDo.getId())
                .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                .eq(ProgressDo::getBizType, "出库")
                .set(ProgressDo::getOperateBy, username)
                .set(ProgressDo::getOperateTime, DateUtil.date())
                .update();
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, outboundDo.getId())
                .set(OrderOutboundDo::getOutboundTime, DateUtil.date())
                .set(OrderOutboundDo::getStatus, NormalOutboundStatusEnum.five.getValue())
                .update();
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, outboundDo.getId())
                .list();
        Map<Long, ProductDo> productIdMatch = productService.lambdaQuery()
                .in(ProductDo::getId, outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList()))
                .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        //锁定的库存扣除
        Set<Long> outProductIdList = outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toSet());
        List<OutboundPickingDo> pickingDoList = outboundPickingService.lambdaQuery()
                .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                .in(OutboundPickingDo::getOutboundProductId, outProductIdList)
                .eq(OutboundPickingDo::getPickFlag, Boolean.TRUE)
                .list();
        //查询库位信息 用于匹配库存属性
        StorageLocationQueryBo slBo = new StorageLocationQueryBo();
        slBo.setIdList(pickingDoList.stream().map(OutboundPickingDo::getRackId).collect(Collectors.toSet()));
        slBo.setTenantId(outboundDo.getTenantId());
        slBo.setPageSize(1000);
        Map<Long, StorageLocationVo> cellIdMatchDo = storageLocationService.getListPage(slBo).getData()
                .stream().collect(Collectors.toMap(StorageLocationVo::getId, val -> val));
        for (OutboundPickingDo pickingDo : pickingDoList.stream().sorted(Comparator.comparing(OutboundPickingDo::getSubStockId)).collect(Collectors.toList())) {
            productStockRackService.lambdaUpdate()
                    .eq(ProductStockRackDo::getId, pickingDo.getSubStockId())
                    .setSql("lock_quantity = lock_quantity - " + pickingDo.getQty())
                    .update();
            Optional<OrderOutboundProductDo> first = outboundProductDoList.stream()
                    .filter(item -> Objects.equals(item.getId(), pickingDo.getOutboundProductId()))
                    .findFirst();
            if (first.isPresent()) {
                OrderOutboundProductDo outboundProductDo = first.get();
                //库存流水
                String batchNo = DateUtils.dateFormatToString(outboundDo.getPushWmsTime(), "yyyyMMdd");
                stockFlowingService.insertProForOut(
                        ProductFlowingOutEt.builder()
                                .sku(productIdMatch.get(outboundProductDo.getProductId()).getSku())
                                .companyId(outboundDo.getCompanyId())
                                .warehouseId(outboundDo.getWhId())
                                .rackId(pickingDo.getRackId())
                                .stockAttribute(cellIdMatchDo.get(pickingDo.getRackId()).getMainType())
                                .bookQuantity(-pickingDo.getQty())
                                .orderType(StockOrderTypeEnum.Outbound.getValue())
                                .noteNo(outboundDo.getOutboundNo())
                                .innerBatchNo(batchNo)
                                .tenantId(outboundDo.getTenantId())
                                .type(StockTypeEnum.one.getValue())
                                .build()
                );
            }
            outboundPickingService.removeById(pickingDo.getId());
        }
        if (Objects.nonNull(outboundDo.getWaveId())) {
            //修改所属波次出库订单数
            waveService.lambdaUpdate()
                    .eq(OutboundWaveDo::getId, outboundDo.getWaveId())
                    .setSql("outbound_order = outbound_order + 1")
                    .update();
            OutboundWaveDo waveDo = waveService.getById(outboundDo.getWaveId());
            if (Objects.equals(waveDo.getOutboundOrder(), waveDo.getOrderNum())) {
                waveService.lambdaUpdate()
                        .eq(OutboundWaveDo::getId, outboundDo.getWaveId())
                        .set(OutboundWaveDo::getOutbound, Boolean.TRUE)
                        .update();
            }
        }
        //忽略进行中的截单，直接出库
        boolean isCancel = cancelService.lambdaUpdate()
                .eq(OutboundCancelDo::getOrderKind, OutboundOrderKind.outbound.getValue())
                .eq(OutboundCancelDo::getOutboundId, outboundDo.getId())
                .eq(OutboundCancelDo::getStatus, 0)
                .set(OutboundCancelDo::getStatus, 2)
                .set(OutboundCancelDo::getFinishTime, DateUtil.date())
                .update();
        if (isCancel) {
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.Order_Interception_Fail,
                    "",
                    outboundDo.getId(),
                    userId,
                    username,
                    OperateTypeEnum.OrderOutbound
            );
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, outboundDo.getId())
                    .set(OrderOutboundDo::getInterceptionFlag, Boolean.FALSE)
                    .update();
        }
//        rabbitTemplate.convertAndSend(RabbitQueueConstant.Outbound_Real_Cost, JSON.toJSONString(outboundDo));
        context.publishEvent(new OutboundAddCostEvent(outboundDo, 0));
//        if (Objects.equals(outboundDo.getOrderOrigin(), OutboundOriginEnum.three.getValue())
//                && StringUtils.isNotEmpty(outboundDo.getPlatformOrderNo())) {
//            PlatOrderSendConsumerReq req = new PlatOrderSendConsumerReq(Collections.singletonList(
//                    new PlatformOrderTrackNoEntity(outboundDo.getId(), outboundDo.getPlatformOrderNo(),
//                            outboundDo.getLogisticsChannelId(), outboundDo.getCarrier(), outboundDo.getTrackingNumber())), userId, outboundDo.getCompanyId());
//            rabbitTemplate.convertAndSend(RabbitQueueConstant.PlatformOrder_Finish, JSON.toJSONString(req));
//        }
    }

    @Override
    public OrderOutCostReq buildOutboundCostReq(OrderOutboundDo outboundDo, Integer status) {
        OrderOutCostReq req = new OrderOutCostReq();
        req.setStatus(status);
        req.setOrderNo(outboundDo.getOutboundNo());
        req.setOrderId(outboundDo.getId());
        req.setWhId(outboundDo.getWhId());
        req.setCompanyId(outboundDo.getCompanyId());
        req.setTenantId(outboundDo.getTenantId());
        req.setOmsOrderCreateTime(outboundDo.getCreateTime());
        switch (outboundDo.getOrderType()) {
            case 1:
                req.setOrderType(StaticDict.Cost_Out_OrderType.First.getValue());
                break;
            case 2:
                req.setOrderType(StaticDict.Cost_Out_OrderType.Second.getValue());
                break;
        }
        req.setTrackId(outboundDo.getLogisticsChannelId());
        req.setInsureFlag(Objects.nonNull(outboundDo.getInsuranceFlag()) ? outboundDo.getInsuranceFlag() : false);
        req.setInsureMoney(Objects.nonNull(outboundDo.getInsureMoney()) ? outboundDo.getInsureMoney().doubleValue() : 0);
        req.setSignatureType(outboundDo.getSignatureService());
        req.setOrderLimit(1);// ?
        req.setProvinceCode(outboundDo.getProvinceName());
        req.setCityName(outboundDo.getCityName());
        req.setWarehouseId(outboundDo.getWhId());
        req.setCountryCode(outboundDo.getCountryRegionCode());
        req.setPostcode(outboundDo.getPostCode());
        List<OrderOutboundProductDo> outboundProductDoList = outboundProductService.lambdaQuery()
                .eq(OrderOutboundProductDo::getOutboundId, outboundDo.getId())
                .list();
        if (CollUtil.isEmpty(outboundProductDoList)) {
            return req;
        }
        List<Long> productIdList = outboundProductDoList.stream().map(OrderOutboundProductDo::getProductId).collect(Collectors.toList());
        Map<Long, ProductDo> productIdMatchDo = new HashMap<>();
        if (CollUtil.isNotEmpty(productIdList)) {
            productIdMatchDo = productService.lambdaQuery()
                    .in(ProductDo::getId, productIdList)
                    .list().stream().collect(Collectors.toMap(ProductDo::getId, val -> val));
        }
        //数据类型 1sku 2箱
        int dataType = Objects.equals(outboundDo.getOrderType(), OutboundTypeEnum.one.getValue())
                ? 1
                : Objects.equals(outboundDo.getOutboundMode(), 1) ? 1 : 2;
        switch (dataType) {
            case 1:
                req.setGoodsType(2);
                List<TrialProductReq> products = new ArrayList<>();
                for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                    ProductDo productDo = productIdMatchDo.get(outboundProductDo.getProductId());
                    if (productDo == null) {
                        continue;
                    }
//                    TrialProductReq trialProductReq = new TrialProductReq();
//                    BeanUtils.copyProperties(productDo, trialProductReq);
                    TrialProductReq trialProductReq = BeanConvert.INSTANCE.productDoToTrialProductReq(productDo);
                    trialProductReq.setProductId(productDo.getId());
                    trialProductReq.setSku(productDo.getSku());
                    trialProductReq.setQuantity(status == 1 ? outboundProductDo.getForecastQty() : outboundProductDo.getOutboundQty());
                    trialProductReq.setCategoryId(productDo.getCategoryId());
                    products.add(trialProductReq);
                }
                req.setProducts(products);
                //查询包裹信息
                List<TrialBoxReq> boxReqList = new ArrayList<>();
                List<OrderOutboundPackageDo> packageDoList = outboundPackageService.lambdaQuery()
                        .eq(OrderOutboundPackageDo::getOutboundId, outboundDo.getId())
                        .list();
                if (CollUtil.isNotEmpty(packageDoList)) {
                    for (OrderOutboundPackageDo packageDo : packageDoList) {
//                        TrialBoxReq boxReq = new TrialBoxReq();
//                        BeanUtils.copyProperties(packageDo, boxReq);
                        TrialBoxReq boxReq = BeanConvert.INSTANCE.packageDoToTrialBoxReq(packageDo);
                        boxReq.setQuantity(1);
                        if (StrUtil.isNotBlank(outboundDo.getTrackingNumber())) {
                            boxReq.setTrackNo(outboundDo.getTrackingNumber());
                        }
                        boxReqList.add(boxReq);
                    }
                } else {
                    //做补足 默认一个包裹
                    //为计算包裹尺寸组装数据
                    Map<Long, Integer> productIdMatchNum = outboundProductDoList.stream().collect(
                            Collectors.toMap(
                                    OrderOutboundProductDo::getProductId,
                                    item -> Objects.equals(item.getOutboundQty(), 0) ? item.getForecastQty() : item.getOutboundQty(),
                                    Integer::sum
                            )
                    );
                    List<PackageSizeVo> packageSizeVoList = new ArrayList<>();
                    if (CollUtil.isEmpty(productIdMatchNum)) {
                        return req;
                    }
                    productService.lambdaQuery()
                            .in(ProductDo::getId, productIdMatchNum.keySet())
                            .list()
                            .forEach(productDo ->
                                    packageSizeVoList.add(
                                            PackageSizeVo.builder()
                                                    .productId(productDo.getId())
                                                    .sku(productDo.getSku())
                                                    .qty(productIdMatchNum.get(productDo.getId()))
                                                    .length(productDo.getWmsLength() == null ? productDo.getLength() : productDo.getWmsLength())
                                                    .width(productDo.getWmsWidth() == null ? productDo.getWidth() : productDo.getWmsWidth())
                                                    .height(productDo.getWmsHeight() == null ? productDo.getHeight() : productDo.getWmsHeight())
                                                    .weight(productDo.getWmsWeight() == null ? productDo.getWeight() : productDo.getWmsWeight())
                                                    .build()
                                    )
                            );
                    OrderOutboundPackageDo packageDo = this.calculatePackageSize(null, 1, null, null, packageSizeVoList, false);
//                    TrialBoxReq boxReq = new TrialBoxReq();
//                    BeanUtils.copyProperties(packageDo, boxReq);
                    TrialBoxReq boxReq = BeanConvert.INSTANCE.packageDoToTrialBoxReq(packageDo);
                    boxReq.setQuantity(1);
                    if (StrUtil.isNotBlank(outboundDo.getTrackingNumber())) {
                        boxReq.setTrackNo(outboundDo.getTrackingNumber());
                    }
                    boxReqList.add(boxReq);
                }
                req.setBoxes(boxReqList);
                break;
            case 2:
                req.setGoodsType(3);
                Map<String, OrderBoxDo> boxMatch = boxService.lambdaQuery()
                        .in(OrderBoxDo::getBoxTypeNumber, outboundProductDoList.stream().map(OrderOutboundProductDo::getBoxTypeNumber).collect(Collectors.toSet()))
                        .groupBy(OrderBoxDo::getBoxTypeNumber)
                        .list().stream().collect(Collectors.toMap(OrderBoxDo::getBoxTypeNumber, val -> val, (o1, o2) -> o1));
                outboundProductDoList = outboundProductDoList.stream().filter(distinctByKey(OrderOutboundProductDo::getBoxTypeNumber)).collect(Collectors.toList());
                List<TrialBoxReq> boxes = new ArrayList<>();
                for (OrderOutboundProductDo outboundProductDo : outboundProductDoList) {
                    OrderBoxDo boxDo = boxMatch.get(outboundProductDo.getBoxTypeNumber());
//                    TrialBoxReq trialBoxReq = new TrialBoxReq();
//                    BeanUtils.copyProperties(boxDo, trialBoxReq);
                    TrialBoxReq trialBoxReq = BeanConvert.INSTANCE.boxDoToTrialBoxReq(boxDo);
                    trialBoxReq.setQuantity(status == 1 ? outboundProductDo.getForecastBoxQty() : outboundProductDo.getOutboundBoxQty());
                    trialBoxReq.setTrackNo(outboundProductDo.getBoxTypeNumber());
                    boxes.add(trialBoxReq);
                }
                req.setBoxes(boxes);
                Collection<OrderOutboundProductDo> values = outboundProductDoList.stream().collect(Collectors.toMap(OrderOutboundProductDo::getProductId, val -> val, (o1, o2) -> {
                    o1.setOutboundQty(o1.getOutboundQty() + o2.getOutboundQty());
                    o1.setForecastQty(o1.getForecastQty() + o2.getForecastQty());
                    return o1;
                })).values();
                List<TrialProductReq> products2 = new ArrayList<>();
                for (OrderOutboundProductDo outboundProductDo : values) {
                    ProductDo productDo = productIdMatchDo.get(outboundProductDo.getProductId());
                    if (Objects.isNull(productDo)) {
                        continue;
                    }
//                    TrialProductReq trialProductReq = new TrialProductReq();
//                    BeanUtils.copyProperties(productDo, trialProductReq);
                    TrialProductReq trialProductReq = BeanConvert.INSTANCE.productDoToTrialProductReq(productDo);
                    trialProductReq.setProductId(productDo.getId());
                    trialProductReq.setSku(productDo.getSku());
                    trialProductReq.setQuantity(outboundProductDo.getOutboundQty());
                    products2.add(trialProductReq);
                }
                req.setProducts(products2);
                break;
            default:
        }
        return req;
    }

    @Override
    public SingleResult getPickerList(Long tenantId) {
        return SingleResult.success(userService.getPickList(tenantId));
    }

    @Override
    public SingleResult assignLogistics(AssignLogisticsBo bo, Long userId, String username) {
        Collection<OrderOutboundDo> outboundDos = this.listByIds(bo.getOutboundIds());
        List<OrderOutboundDo> outboundDoList = outboundDos.stream().filter(item -> Objects.equals(item.getLogisticsChannelId(), bo.getLogisticsChannelId())).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(outboundDoList)) {
            OrderOutboundDo outboundDo = outboundDoList.get(0);
            throw new BizException(SysConstant.Outbound_Assign_Logistics_Exist_Same, outboundDo.getOutboundNo());
        }
        Set<Long> channelIds = outboundDos.stream().map(OrderOutboundDo::getLogisticsChannelId).collect(Collectors.toSet());
        channelIds.add(bo.getLogisticsChannelId());
        Map<Long, LogisticsChannelDo> channelMatch = logisticsChannelService.lambdaQuery()
                .in(LogisticsChannelDo::getId, channelIds)
                .list().stream().collect(Collectors.toMap(LogisticsChannelDo::getId, val -> val));
        for (OrderOutboundDo outboundDo : outboundDos) {
            this.lambdaUpdate()
                    .set(OrderOutboundDo::getLogisticsChannelId, bo.getLogisticsChannelId())
                    .eq(OrderOutboundDo::getId, outboundDo.getId())
                    .update();
            operateRecordService.operateAddRecordAndValue(
                    SysConstant.Assign_Logistics,
                    channelMatch.get(outboundDo.getLogisticsChannelId()).getChannelCode() + SysConstant.operateRecordSeparator + channelMatch.get(bo.getLogisticsChannelId()).getChannelCode(),
                    outboundDo.getId(),
                    userId,
                    username,
                    OperateTypeEnum.OrderOutbound
            );
        }
        return SingleResult.success();
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public CommonBatchOperationVo submitDraft(OutboundSubmitDraftBo bo, Long tenantId, Long userId, String username) {
        CommonBatchOperationVo res = new CommonBatchOperationVo();
        if (CollUtil.isEmpty(bo.getOutboundIdList())) {
            return res;
        }
        res.setTotal(bo.getOutboundIdList().size());
        Map<Long, OrderOutboundDo> outboundDoMap = this.listByIds(bo.getOutboundIdList()).stream().collect(Collectors.toMap(OrderOutboundDo::getId, orderOutboundDo -> orderOutboundDo));
        Map<Long, List<OrderOutboundProductDo>> outIdMatchProducts = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, bo.getOutboundIdList())
                .list().stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getOutboundId));
        String key = RedisKeyConstants.OUTBOUND_SUBMIT_DRAFT;
        for (Long id : bo.getOutboundIdList()) {
            OrderOutboundDo outboundDo = outboundDoMap.get(id);
            if (Objects.isNull(outboundDo)) {
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", id);
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Outbound_Order_Does_Not_Exist));
                res.setFailed(res.getFailed() + 1);
                res.getFailedList().add(map);
                continue;
            }
            if (outboundDo.getStatus() != 0) {
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", outboundDo.getOutboundNo());
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Outbound_Order_No_Draft));
                res.setFailed(res.getFailed() + 1);
                res.getFailedList().add(map);
                continue;
            }
            List<OrderOutboundProductDo> outboundProductDoList = outIdMatchProducts.get(id);
            Integer orderType = outboundDo.getOrderType();
            RLock lock = redissonClient.getLock(key + outboundDo.getId());
            boolean isLocked = false;
            try {
                //最多等锁3秒，最多持有锁30秒
                isLocked = lock.tryLock(3, 60, TimeUnit.SECONDS);
                if (isLocked) {
                    // 业务逻辑
                    Boolean executeStatus = transactionTemplate.execute(status -> {
                        try {
                            OrderOutboundDo checkStatus = this.getById(id);
                            if (checkStatus.getStatus() != 0) {
                                throw new BizException(SysConstant.Outbound_Order_No_Draft);
                            }
                            switch (orderType) {
                                case 1:
                                    //提交一件代发出库单后续锁定库存和下单校验操作
                                    String[] failMsg1 = this.submitNormalOutboundToWmsAfterLockHandler(outboundDo, tenantId, outboundProductDoList);
                                    if (ArrayUtil.isNotEmpty(failMsg1)) {
                                        throw new BizException(Arrays.stream(failMsg1).map(StringsUtil::createI18Message).collect(Collectors.joining(";")));
                                    }
                                    //提交出库单后置操作
                                    this.pushOrderOutboundToWmsAfterHandler(outboundDo, tenantId, userId, username);
                                    break;
                                case 2:
                                    //提交备货中转出库单后续锁定库存和下单校验操作
                                    String[] failMsg2 = this.submitTransferOutboundToWmsAfterLockHandler(outboundDo, tenantId, outboundProductDoList);
                                    if (ArrayUtil.isNotEmpty(failMsg2)) {
                                        throw new BizException(Arrays.stream(failMsg2).map(StringsUtil::createI18Message).collect(Collectors.joining(";")));
                                    }
                                    //提交出库单后置操作
                                    this.pushOrderOutboundToWmsAfterHandler(outboundDo, tenantId, userId, username);
                                    break;
                                default:
                                    break;
                            }
                            return true;
                        } catch (Exception e) {
                            log.error("batch submit draft failed,outboundId[" + outboundDo.getId() + "]", e);
                            // 手动标记回滚
                            status.setRollbackOnly();
                            String msg = SysConstant.Try_Again_Later;
                            if (e instanceof BizException) {
                                msg = e.getMessage();
                            }
                            HashMap<String, Object> map = new HashMap<>(4);
                            map.put("code", outboundDo.getOutboundNo());
                            map.put("errorMsg", StringsUtil.createI18Message(msg));
                            res.getFailedList().add(map);
                            res.setFailed(res.getFailed() + 1);
                            return false;
                        }
                    });
                    if (Boolean.TRUE.equals(executeStatus)) {
                        res.setSuccess(res.getSuccess() + 1);
                    }
                } else {
                    throw new BizException(SysConstant.Outbound_Processing);
                }
            } catch (Exception e) {
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", outboundDo.getOutboundNo());
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Outbound_Processing));
                res.getFailedList().add(map);
                res.setFailed(res.getFailed() + 1);
            } finally {
                if (isLocked && lock.isHeldByCurrentThread()) {
                    lock.unlock(); // 解锁
                }
            }
        }
        return res;
    }

    @Override
    public CommonBatchOperationVo turnIntoDraft(OutboundTurnIntoDraftBo bo, Long userId, String username) {
        CommonBatchOperationVo res = new CommonBatchOperationVo();
        res.setTotal(bo.getOutboundIdList().size());
        Map<Long, OrderOutboundDo> outboundDoMap = this.listByIds(bo.getOutboundIdList()).stream().collect(Collectors.toMap(OrderOutboundDo::getId, orderOutboundDo -> orderOutboundDo));
        Map<Long, List<OrderOutboundProductDo>> outOrderIdMatchProductDoList = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoMap.keySet())
                .list().stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getOutboundId));
        for (Long id : bo.getOutboundIdList()) {
            OrderOutboundDo outboundDo = outboundDoMap.get(id);
            if (Objects.isNull(outboundDo)) {
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", id);
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Outbound_Order_Does_Not_Exist));
                res.setFailed(res.getFailed() + 1);
                res.getFailedList().add(map);
                continue;
            }
            if (bo.getType() == 0) {
                if (Objects.equals(outboundDo.getOrderType(), 1) && !NormalOutboundStatusEnum.six.getValue().equals(outboundDo.getStatus()) || Objects.equals(outboundDo.getOrderType(), 2) && !TransferOutboundStatusEnum.five.getValue().equals(outboundDo.getStatus())) {
                    HashMap<String, Object> map = new HashMap<>(4);
                    map.put("code", outboundDo.getOutboundNo());
                    map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Status_Error));
                    res.setFailed(res.getFailed() + 1);
                    res.getFailedList().add(map);
                    continue;
                }
            } else if (bo.getType() == 1) {
                if (Objects.equals(outboundDo.getOrderType(), 1) && !NormalOutboundStatusEnum.seven.getValue().equals(outboundDo.getStatus()) || Objects.equals(outboundDo.getOrderType(), 2) && !TransferOutboundStatusEnum.six.getValue().equals(outboundDo.getStatus())) {
                    HashMap<String, Object> map = new HashMap<>(4);
                    map.put("code", outboundDo.getOutboundNo());
                    map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Status_Error));
                    res.setFailed(res.getFailed() + 1);
                    res.getFailedList().add(map);
                    continue;
                }
            }
            this.lambdaUpdate()
                    .eq(OrderOutboundDo::getId, id)
                    .set(OrderOutboundDo::getStatus, 0)
                    .set(bo.getType() == 0, OrderOutboundDo::getMarkExceptionTime, null)
                    .set(bo.getType() == 0, OrderOutboundDo::getExceptionTime, null)
                    .set(bo.getType() == 0, OrderOutboundDo::getExceptionReason, null)
                    .set(bo.getType() == 0, OrderOutboundDo::getExceptionType, null)
                    .set(bo.getType() == 1, OrderOutboundDo::getCancelTime, null)
                    .set(bo.getType() == 1, OrderOutboundDo::getInterceptionFlag, Boolean.FALSE)
                    .update();
            //异常订单如存在锁定库存需要释放
            List<OrderOutboundProductDo> outboundProductDoList = outOrderIdMatchProductDoList.get(id);
            List<OutboundPickingDo> pickingDoList = outboundPickingService.lambdaQuery()
                    .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                    .in(OutboundPickingDo::getOutboundProductId, outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                    .list();
            if (CollUtil.isNotEmpty(pickingDoList)) {
                this.unLock(
                        JSONUtil.toList(JSONUtil.toJsonStr(pickingDoList), OutboundPickingDo.class),
                        pickingDoList,
                        new AtomicInteger(pickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum))
                );
            }
            res.setSuccess(res.getSuccess() + 1);
        }
        return res;
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public CommonBatchOperationVo cancel(OutboundCancelBo bo, Long userId, String username, Long companyId) {
        CommonBatchOperationVo res = new CommonBatchOperationVo();
        res.setTotal(bo.getOutboundIdList().size());
        Map<Long, OrderOutboundDo> outboundDoMap = this.listByIds(bo.getOutboundIdList()).stream().collect(Collectors.toMap(OrderOutboundDo::getId, orderOutboundDo -> orderOutboundDo));
        Map<Long, List<OrderOutboundProductDo>> outOrderIdMatchProductDoList = outboundProductService.lambdaQuery()
                .in(OrderOutboundProductDo::getOutboundId, outboundDoMap.keySet())
                .list().stream().collect(Collectors.groupingBy(OrderOutboundProductDo::getOutboundId));
        List<PlatOrderCancelConsumerReq> cancalPlatOder = new ArrayList<>();
        String key = RedisKeyConstants.OUTBOUND_CANCEL;
        for (Long id : bo.getOutboundIdList()) {
            OrderOutboundDo outboundDo = outboundDoMap.get(id);
            if (Objects.isNull(outboundDo)) {
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", id);
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Outbound_Order_Does_Not_Exist));
                res.setFailed(res.getFailed() + 1);
                res.getFailedList().add(map);
                continue;
            }
            if ((Objects.equals(outboundDo.getOrderType(), 1) && !NormalOutboundStatusEnum.six.getValue().equals(outboundDo.getStatus()) || Objects.equals(outboundDo.getOrderType(), 2) && !TransferOutboundStatusEnum.five.getValue().equals(outboundDo.getStatus())) && !Objects.equals(outboundDo.getStatus(), 0)) {
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", outboundDo.getOutboundNo());
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Outbound_Status_Cannot_Cancel));
                res.setFailed(res.getFailed() + 1);
                res.getFailedList().add(map);
                continue;
            }
            RLock lock = redissonClient.getLock(key + outboundDo.getId());
            boolean isLocked = false;
            try {
                //最多等锁3秒，最多持有锁20秒
                isLocked = lock.tryLock(3, 20, TimeUnit.SECONDS);
                if (isLocked) {
                    // 业务逻辑
                    Boolean executeStatus = transactionTemplate.execute(status -> {
                        try {
                            operateRecordService.operateAddRecordAndValue(
                                    SysConstant.OrderOutbound_Cancel,
                                    "",
                                    outboundDo.getId(),
                                    userId,
                                    username,
                                    OperateTypeEnum.OrderOutbound
                            );
                            progressService.remove(
                                    Wrappers.<ProgressDo>lambdaQuery()
                                            .eq(ProgressDo::getBizNumber, outboundDo.getId())
                                            .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                                            .ne(ProgressDo::getBizType, "创建")
                            );
                            progressService.save(
                                    ProgressDo.builder()
                                            .bizNumber(outboundDo.getId())
                                            .type(ProgressEnum.OUTBOUND.getTypeCode())
                                            .bizType("取消")
                                            .operateBy(username)
                                            .operateTime(DateUtil.date())
                                            .sort(2)
                                            .build());
                            this.lambdaUpdate()
                                    .eq(OrderOutboundDo::getId, id)
                                    .set(OrderOutboundDo::getStatus, outboundDo.getOrderType().equals(1) ? NormalOutboundStatusEnum.seven.getValue() : TransferOutboundStatusEnum.six.getValue())
                                    .set(OrderOutboundDo::getCancelTime, DateUtil.date())
                                    .set(OrderOutboundDo::getExceptionTime, DateUtil.date())
                                    .update();
                            //异常订单如存在锁定库存需要释放
                            List<OrderOutboundProductDo> outboundProductDoList = outOrderIdMatchProductDoList.get(id);
                            List<OutboundPickingDo> pickingDoList = outboundPickingService.lambdaQuery()
                                    .eq(OutboundPickingDo::getOrderType, OutboundOrderKind.outbound.getValue())
                                    .in(OutboundPickingDo::getOutboundProductId, outboundProductDoList.stream().map(OrderOutboundProductDo::getId).collect(Collectors.toList()))
                                    .list();
                            if (CollUtil.isNotEmpty(pickingDoList)) {
                                this.unLock(
                                        JSONUtil.toList(JSONUtil.toJsonStr(pickingDoList), OutboundPickingDo.class),
                                        pickingDoList,
                                        new AtomicInteger(pickingDoList.stream().map(OutboundPickingDo::getQty).reduce(0, Integer::sum))
                                );
                            }
                            return true;
                        } catch (Exception e) {
                            log.error("batch cancel outbound failed,outboundId[" + outboundDo.getId() + "]", e);
                            // 手动标记回滚
                            status.setRollbackOnly();
                            String msg = SysConstant.Try_Again_Later;
                            if (e instanceof BizException) {
                                msg = e.getMessage();
                            }
                            HashMap<String, Object> map = new HashMap<>(4);
                            map.put("code", outboundDo.getOutboundNo());
                            map.put("errorMsg", StringsUtil.createI18Message(msg));
                            res.getFailedList().add(map);
                            res.setFailed(res.getFailed() + 1);
                            return false;
                        }
                    });
                    if (Boolean.TRUE.equals(executeStatus)) {
                        res.setSuccess(res.getSuccess() + 1);
                        if (Objects.equals(outboundDo.getOrderOrigin(), OutboundOriginEnum.three.getValue()) && StringUtils.isNotBlank(outboundDo.getPlatformOrderNo())) {
                            cancalPlatOder.add(new PlatOrderCancelConsumerReq(outboundDo.getPlatformOrderNo(), id, userId, companyId));
                        }
                    }
                } else {
                    throw new BizException(SysConstant.Outbound_Processing);
                }
            } catch (InterruptedException e) {
                HashMap<String, Object> map = new HashMap<>(4);
                map.put("code", outboundDo.getOutboundNo());
                map.put("errorMsg", StringsUtil.createI18Message(SysConstant.Outbound_Processing));
                res.getFailedList().add(map);
                res.setFailed(res.getFailed() + 1);
            } finally {
                if (isLocked && lock.isHeldByCurrentThread()) {
                    lock.unlock(); // 解锁
                }
            }
        }
        if (!cancalPlatOder.isEmpty()) {
//            PlatOrderCancelConsumerReq req = new PlatOrderCancelConsumerReq(cancalPlatOder, userId, companyId);
            for (PlatOrderCancelConsumerReq req : cancalPlatOder) {
                rabbitTemplate.convertAndSend(RabbitQueueConstant.PlatformOrder_Cancel, JSON.toJSONString(req));
            }
        }
        return res;
    }

    @Override
    public SingleResult uploadAttachment(OutboundUploadAttachmentBo bo, Long userId, String username) {
        OrderOutboundDo outboundDo = this.getById(bo.getOutboundId());
        String appendixIds = outboundDo.getAppendixIds();
        if (StrUtil.isNotBlank(appendixIds)) {
            List<String> newIdList = Arrays.asList(bo.getFileIds().split(","));
            List<String> oldIdList = Arrays.asList(appendixIds.split(","));
            List<String> addList = newIdList.stream().filter(val -> !oldIdList.contains(val)).collect(Collectors.toList());
            List<String> deleteList = oldIdList.stream().filter(val -> !newIdList.contains(val)).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(addList)) {
                //上传到正式目录
                fileService.uploadFileMult(addList.toArray(new String[0]));
            }
            if (CollUtil.isNotEmpty(deleteList)) {
                //删除
                fileService.deleteFileMulti(deleteList.toArray(new String[0]));
            }
        } else {
            //上传到正式目录
            fileService.uploadFileMult(bo.getFileIds().split(","));
        }
        outboundDo.setAppendixIds(bo.getFileIds());
        this.updateById(outboundDo);
        operateRecordService.operateAddRecordAndValue(
                SysConstant.OrderOutbound_Upload_Attachment,
                "",
                outboundDo.getId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        return SingleResult.success();
    }


    @Override
    public List<String> batchUpdateStatus(List<String> outboundNoList, Integer status, int orderType, Long whId, Long userId, String username) {
        List<Integer> statusList = orderType == 0 ? Arrays.asList(NormalOutboundStatusEnum.one.getValue(), NormalOutboundStatusEnum.two.getValue(), NormalOutboundStatusEnum.three.getValue(), NormalOutboundStatusEnum.four.getValue()) : Arrays.asList(TransferOutboundStatusEnum.one.getValue(), TransferOutboundStatusEnum.two.getValue(), TransferOutboundStatusEnum.three.getValue());
        if (!statusList.contains(status) || outboundNoList.isEmpty()) {
            return Collections.emptyList();
        }
        List<OrderOutboundDo> outboundDoList = this.lambdaQuery()
                .select(OrderOutboundDo::getId, OrderOutboundDo::getOutboundNo)
                .in(OrderOutboundDo::getOutboundNo, outboundNoList)
                .eq(OrderOutboundDo::getWhId, whId)
                .in(OrderOutboundDo::getStatus, statusList)
                .list();
        if (outboundDoList.isEmpty()) {
            return Collections.emptyList();
        }
        this.lambdaUpdate()
                .in(OrderOutboundDo::getId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .eq(OrderOutboundDo::getWhId, whId)
                .in(OrderOutboundDo::getStatus, statusList)
                .set(OrderOutboundDo::getStatus, status)
                .update();
        //操作日志
        List<OperateRecordDo> operateRecordDos = new ArrayList<>();
        for (OrderOutboundDo order : outboundDoList) {
            OperateRecordDo operateRecordDo = new OperateRecordDo(order.getId(), OperateTypeEnum.OrderOutbound.getValue(), SysConstant.platformOrder_Update_Status, userId, new Date(), NormalOutboundStatusEnum.getDescByValue(status));
            operateRecordDos.add(operateRecordDo);
        }
        rabbitTemplate.convertAndSend(RabbitQueueConstant.CrossMall_CMOrder_OperateList, JSON.toJSONString(operateRecordDos));
        return outboundDoList.stream().map(OrderOutboundDo::getOutboundNo).collect(Collectors.toList());
    }


    @Override
    public List<String> exceptionOrder(List<String> outboundNoList, String exceptionType, String exceptionReason, int orderType, Long whId, Long userId) {
        if (CollUtil.isEmpty(outboundNoList)) {
            return Collections.emptyList();
        }
        List<Integer> statusList = orderType == 0 ? Arrays.asList(NormalOutboundStatusEnum.one.getValue(), NormalOutboundStatusEnum.two.getValue(), NormalOutboundStatusEnum.three.getValue(), NormalOutboundStatusEnum.four.getValue()) : Arrays.asList(TransferOutboundStatusEnum.one.getValue(), TransferOutboundStatusEnum.two.getValue(), TransferOutboundStatusEnum.three.getValue());
        List<OrderOutboundDo> outboundDoList = this.lambdaQuery()
                .select(OrderOutboundDo::getId, OrderOutboundDo::getOutboundNo)
                .in(OrderOutboundDo::getOutboundNo, outboundNoList)
                .eq(OrderOutboundDo::getWhId, whId)
                .in(OrderOutboundDo::getStatus, statusList)
                .list();
        if (outboundDoList.isEmpty()) {
            return Collections.emptyList();
        }
        if (StringUtils.isNotBlank(exceptionReason) && exceptionReason.length() > 100) {
            exceptionReason = exceptionReason.substring(0, 100);
        }
        this.lambdaUpdate()
                .in(OrderOutboundDo::getId, outboundDoList.stream().map(OrderOutboundDo::getId).collect(Collectors.toList()))
                .eq(OrderOutboundDo::getWhId, whId)
                .in(OrderOutboundDo::getStatus, statusList)
                .set(OrderOutboundDo::getStatus, orderType == 0 ? NormalOutboundStatusEnum.six.getValue() : TransferOutboundStatusEnum.five.getValue())
                .set(OrderOutboundDo::getExceptionType, exceptionType)
                .set(OrderOutboundDo::getExceptionReason, exceptionReason)
                .set(OrderOutboundDo::getExceptionTime, new Date())
                .update();
        //操作日志
        List<OperateRecordDo> operateRecordDos = new ArrayList<>();
        for (OrderOutboundDo order : outboundDoList) {
            OperateRecordDo operateRecordDo = new OperateRecordDo(order.getId(), OperateTypeEnum.OrderOutbound.getValue(), SysConstant.platformOrder_MarkException, userId, new Date(), exceptionReason);
            operateRecordDos.add(operateRecordDo);
        }
        rabbitTemplate.convertAndSend(RabbitQueueConstant.CrossMall_CMOrder_OperateList, JSON.toJSONString(operateRecordDos));
        return outboundDoList.stream().map(OrderOutboundDo::getOutboundNo).collect(Collectors.toList());
    }

    @Override
    public SingleResult transferOutboundOperationApi(TransferSendApiBo bo, Long userId, String username) {
        OrderOutboundDo outboundDo = this.lambdaQuery()
                .eq(OrderOutboundDo::getOutboundNo, bo.getOutboundNo())
                .one();
        if (Objects.isNull(outboundDo)) {
            return SingleResult.failure(SysConstant.Not_Found_Outbound_Fail, bo.getOutboundNo());
        }
        if (!Objects.equals(outboundDo.getOrderType(), OutboundTypeEnum.two.getValue())) {
            return SingleResult.failure(SysConstant.Order_type_Error, bo.getOutboundNo());
        }
        if (!(outboundDo.getStatus() >= 1 && outboundDo.getStatus() <= 3)) {
            return SingleResult.failure(SysConstant.Status_Error, bo.getOutboundNo());

        }
        if (Objects.nonNull(outboundDo.getMarkExceptionTime())) {
            return SingleResult.failure(SysConstant.Marked_Exception_Outbound_Fail, bo.getOutboundNo());
        }
        progressService.lambdaUpdate()
                .eq(ProgressDo::getBizNumber, outboundDo.getId())
                .eq(ProgressDo::getType, ProgressEnum.OUTBOUND.getTypeCode())
                .isNull(ProgressDo::getOperateTime)
                .set(ProgressDo::getOperateBy, username)
                .set(ProgressDo::getOperateTime, DateUtil.date())
                .update();
        this.transferOutboundOperateAfterHandler(outboundDo, userId, username, bo.getTrackingNumber(), null, null, null, true);
        return SingleResult.success();
    }

    @Override
    public SingleResult outboundUpdateChannelAndTrackingNo(OutboundUpdateChannelAndTrackingNoBo bo, Long userId, String username) {
        this.lambdaUpdate()
                .eq(OrderOutboundDo::getId, bo.getOutboundId())
                .set(StrUtil.isNotBlank(bo.getTrackingNumber()), OrderOutboundDo::getTrackingNumber, bo.getTrackingNumber())
                .set(Objects.nonNull(bo.getLogisticsChannelId()), OrderOutboundDo::getLogisticsChannelId, bo.getLogisticsChannelId())
                .set(StrUtil.isNotBlank(bo.getCarrier()), OrderOutboundDo::getCarrier, bo.getCarrier())
                .set(StrUtil.isNotBlank(bo.getCustomizeChannelName()), OrderOutboundDo::getCustomizeChannelName, bo.getCustomizeChannelName())
                .update();
        operateRecordService.operateAddRecordAndValue(
                SysConstant.Outbound_Update_Channel_Tracking_Number,
                "",
                bo.getOutboundId(),
                userId,
                username,
                OperateTypeEnum.OrderOutbound
        );
        return SingleResult.success();
    }
}
